Merge remote-tracking branch 'origin' into success-icon-consistency
This commit is contained in:
commit
96fa6e7830
110 changed files with 3976 additions and 1651 deletions
10
.github/CONTRIBUTING.md
vendored
10
.github/CONTRIBUTING.md
vendored
|
|
@ -26,11 +26,15 @@ Prerequisites:
|
|||
- Go 1.13+ for building the binary
|
||||
- Go 1.15+ for running the test suite
|
||||
|
||||
Build with: `make` or `go build -o bin/gh ./cmd/gh`
|
||||
Build with:
|
||||
* Unix-like systems: `make`
|
||||
* Windows: `go run script/build.go`
|
||||
|
||||
Run the new binary as: `./bin/gh`
|
||||
Run the new binary as:
|
||||
* Unix-like systems: `bin/gh`
|
||||
* Windows: `bin\gh`
|
||||
|
||||
Run tests with: `make test` or `go test ./...`
|
||||
Run tests with: `go test ./...`
|
||||
|
||||
## Submitting a pull request
|
||||
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -6,6 +6,7 @@
|
|||
/site
|
||||
.github/**/node_modules
|
||||
/CHANGELOG.md
|
||||
/script/build
|
||||
|
||||
# VS Code
|
||||
.vscode
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
linters:
|
||||
enable:
|
||||
gofmt
|
||||
- gofmt
|
||||
- nolintlint
|
||||
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
|
|
|
|||
47
Makefile
47
Makefile
|
|
@ -1,14 +1,3 @@
|
|||
BUILD_FILES = $(shell go list -f '{{range .GoFiles}}{{$$.Dir}}/{{.}}\
|
||||
{{end}}' ./...)
|
||||
|
||||
GH_VERSION ?= $(shell git describe --tags 2>/dev/null || git rev-parse --short HEAD)
|
||||
DATE_FMT = +%Y-%m-%d
|
||||
ifdef SOURCE_DATE_EPOCH
|
||||
BUILD_DATE ?= $(shell date -u -d "@$(SOURCE_DATE_EPOCH)" "$(DATE_FMT)" 2>/dev/null || date -u -r "$(SOURCE_DATE_EPOCH)" "$(DATE_FMT)" 2>/dev/null || date -u "$(DATE_FMT)")
|
||||
else
|
||||
BUILD_DATE ?= $(shell date "$(DATE_FMT)")
|
||||
endif
|
||||
|
||||
CGO_CPPFLAGS ?= ${CPPFLAGS}
|
||||
export CGO_CPPFLAGS
|
||||
CGO_CFLAGS ?= ${CFLAGS}
|
||||
|
|
@ -16,27 +5,34 @@ export CGO_CFLAGS
|
|||
CGO_LDFLAGS ?= $(filter -g -L% -l% -O%,${LDFLAGS})
|
||||
export CGO_LDFLAGS
|
||||
|
||||
GO_LDFLAGS := -X github.com/cli/cli/internal/build.Version=$(GH_VERSION) $(GO_LDFLAGS)
|
||||
GO_LDFLAGS := -X github.com/cli/cli/internal/build.Date=$(BUILD_DATE) $(GO_LDFLAGS)
|
||||
ifdef GH_OAUTH_CLIENT_SECRET
|
||||
GO_LDFLAGS := -X github.com/cli/cli/internal/authflow.oauthClientID=$(GH_OAUTH_CLIENT_ID) $(GO_LDFLAGS)
|
||||
GO_LDFLAGS := -X github.com/cli/cli/internal/authflow.oauthClientSecret=$(GH_OAUTH_CLIENT_SECRET) $(GO_LDFLAGS)
|
||||
endif
|
||||
## The following tasks delegate to `script/build.go` so they can be run cross-platform.
|
||||
|
||||
bin/gh: $(BUILD_FILES)
|
||||
go build -trimpath -ldflags "${GO_LDFLAGS}" -o "$@" ./cmd/gh
|
||||
.PHONY: bin/gh
|
||||
bin/gh: script/build
|
||||
@script/build bin/gh
|
||||
|
||||
script/build: script/build.go
|
||||
go build -o script/build script/build.go
|
||||
|
||||
clean:
|
||||
rm -rf ./bin ./share
|
||||
.PHONY: clean
|
||||
clean: script/build
|
||||
@script/build clean
|
||||
|
||||
.PHONY: manpages
|
||||
manpages: script/build
|
||||
@script/build manpages
|
||||
|
||||
# just a convenience task around `go test`
|
||||
.PHONY: test
|
||||
test:
|
||||
go test ./...
|
||||
.PHONY: test
|
||||
|
||||
## Site-related tasks are exclusively intended for use by the GitHub CLI team and for our release automation.
|
||||
|
||||
site:
|
||||
git clone https://github.com/github/cli.github.com.git "$@"
|
||||
|
||||
.PHONY: site-docs
|
||||
site-docs: site
|
||||
git -C site pull
|
||||
git -C site rm 'manual/gh*.md' 2>/dev/null || true
|
||||
|
|
@ -44,8 +40,8 @@ site-docs: site
|
|||
rm -f site/manual/*.bak
|
||||
git -C site add 'manual/gh*.md'
|
||||
git -C site commit -m 'update help docs' || true
|
||||
.PHONY: site-docs
|
||||
|
||||
.PHONY: site-bump
|
||||
site-bump: site-docs
|
||||
ifndef GITHUB_REF
|
||||
$(error GITHUB_REF is not set)
|
||||
|
|
@ -53,11 +49,8 @@ endif
|
|||
sed -i.bak -E 's/(assign version = )".+"/\1"$(GITHUB_REF:refs/tags/v%=%)"/' site/index.html
|
||||
rm -f site/index.html.bak
|
||||
git -C site commit -m '$(GITHUB_REF:refs/tags/v%=%)' index.html
|
||||
.PHONY: site-bump
|
||||
|
||||
.PHONY: manpages
|
||||
manpages:
|
||||
go run ./cmd/gen-docs --man-page --doc-path ./share/man/man1/
|
||||
## Install/uninstall tasks are here for use on *nix platform. On Windows, there is no equivalent.
|
||||
|
||||
DESTDIR :=
|
||||
prefix := /usr/local
|
||||
|
|
|
|||
|
|
@ -5,19 +5,12 @@ import (
|
|||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func eq(t *testing.T, got interface{}, expected interface{}) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(got, expected) {
|
||||
t.Errorf("expected: %v, got: %v", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGraphQL(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
client := NewClient(
|
||||
|
|
@ -38,13 +31,13 @@ func TestGraphQL(t *testing.T) {
|
|||
)
|
||||
|
||||
err := client.GraphQL("github.com", "QUERY", vars, &response)
|
||||
eq(t, err, nil)
|
||||
eq(t, response.Viewer.Login, "hubot")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hubot", response.Viewer.Login)
|
||||
|
||||
req := http.Requests[0]
|
||||
reqBody, _ := ioutil.ReadAll(req.Body)
|
||||
eq(t, string(reqBody), `{"query":"QUERY","variables":{"name":"Mona"}}`)
|
||||
eq(t, req.Header.Get("Authorization"), "token OTOKEN")
|
||||
assert.Equal(t, `{"query":"QUERY","variables":{"name":"Mona"}}`, string(reqBody))
|
||||
assert.Equal(t, "token OTOKEN", req.Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
func TestGraphQLError(t *testing.T) {
|
||||
|
|
@ -84,7 +77,7 @@ func TestRESTGetDelete(t *testing.T) {
|
|||
|
||||
r := bytes.NewReader([]byte(`{}`))
|
||||
err := client.REST("github.com", "DELETE", "applications/CLIENTID/grant", r, nil)
|
||||
eq(t, err, nil)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRESTError(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package api
|
|||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPullRequest_ChecksStatus(t *testing.T) {
|
||||
|
|
@ -31,11 +33,11 @@ func TestPullRequest_ChecksStatus(t *testing.T) {
|
|||
} }] } }
|
||||
`
|
||||
err := json.Unmarshal([]byte(payload), &pr)
|
||||
eq(t, err, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
checks := pr.ChecksStatus()
|
||||
eq(t, checks.Total, 8)
|
||||
eq(t, checks.Pending, 3)
|
||||
eq(t, checks.Failing, 3)
|
||||
eq(t, checks.Passing, 2)
|
||||
assert.Equal(t, 8, checks.Total)
|
||||
assert.Equal(t, 3, checks.Pending)
|
||||
assert.Equal(t, 3, checks.Failing)
|
||||
assert.Equal(t, 2, checks.Passing)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,3 +132,51 @@ func CommentCreate(client *Client, repoHost string, params CommentCreateInput) (
|
|||
|
||||
return mutation.AddComment.CommentEdge.Node.URL, nil
|
||||
}
|
||||
|
||||
func commentsFragment() string {
|
||||
return `comments(last: 1) {
|
||||
nodes {
|
||||
author {
|
||||
login
|
||||
}
|
||||
authorAssociation
|
||||
body
|
||||
createdAt
|
||||
includesCreatedEdit
|
||||
` + reactionGroupsFragment() + `
|
||||
}
|
||||
totalCount
|
||||
}`
|
||||
}
|
||||
|
||||
func (c Comment) AuthorLogin() string {
|
||||
return c.Author.Login
|
||||
}
|
||||
|
||||
func (c Comment) Association() string {
|
||||
return c.AuthorAssociation
|
||||
}
|
||||
|
||||
func (c Comment) Content() string {
|
||||
return c.Body
|
||||
}
|
||||
|
||||
func (c Comment) Created() time.Time {
|
||||
return c.CreatedAt
|
||||
}
|
||||
|
||||
func (c Comment) IsEdited() bool {
|
||||
return c.IncludesCreatedEdit
|
||||
}
|
||||
|
||||
func (c Comment) Reactions() ReactionGroups {
|
||||
return c.ReactionGroups
|
||||
}
|
||||
|
||||
func (c Comment) Status() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c Comment) Link() string {
|
||||
return ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -481,3 +481,11 @@ func milestoneNodeIdToDatabaseId(nodeId string) (string, error) {
|
|||
}
|
||||
return splitted[1], nil
|
||||
}
|
||||
|
||||
func (i Issue) Link() string {
|
||||
return i.URL
|
||||
}
|
||||
|
||||
func (i Issue) Identifier() string {
|
||||
return i.ID
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,19 +15,6 @@ import (
|
|||
"github.com/shurcooL/githubv4"
|
||||
)
|
||||
|
||||
type PullRequestReviewState int
|
||||
|
||||
const (
|
||||
ReviewApprove PullRequestReviewState = iota
|
||||
ReviewRequestChanges
|
||||
ReviewComment
|
||||
)
|
||||
|
||||
type PullRequestReviewInput struct {
|
||||
Body string
|
||||
State PullRequestReviewState
|
||||
}
|
||||
|
||||
type PullRequestsPayload struct {
|
||||
ViewerCreated PullRequestAndTotalCount
|
||||
ReviewRequested PullRequestAndTotalCount
|
||||
|
|
@ -103,14 +90,6 @@ type PullRequest struct {
|
|||
}
|
||||
TotalCount int
|
||||
}
|
||||
Reviews struct {
|
||||
Nodes []struct {
|
||||
Author struct {
|
||||
Login string
|
||||
}
|
||||
State string
|
||||
}
|
||||
}
|
||||
Assignees struct {
|
||||
Nodes []struct {
|
||||
Login string
|
||||
|
|
@ -139,6 +118,7 @@ type PullRequest struct {
|
|||
}
|
||||
Comments Comments
|
||||
ReactionGroups ReactionGroups
|
||||
Reviews PullRequestReviews
|
||||
}
|
||||
|
||||
type NotFoundError struct {
|
||||
|
|
@ -156,6 +136,14 @@ func (pr PullRequest) HeadLabel() string {
|
|||
return pr.HeadRefName
|
||||
}
|
||||
|
||||
func (pr PullRequest) Link() string {
|
||||
return pr.URL
|
||||
}
|
||||
|
||||
func (pr PullRequest) Identifier() string {
|
||||
return pr.ID
|
||||
}
|
||||
|
||||
type PullRequestReviewStatus struct {
|
||||
ChangesRequested bool
|
||||
Approved bool
|
||||
|
|
@ -220,6 +208,18 @@ func (pr *PullRequest) ChecksStatus() (summary PullRequestChecksStatus) {
|
|||
return
|
||||
}
|
||||
|
||||
func (pr *PullRequest) DisplayableReviews() PullRequestReviews {
|
||||
published := []PullRequestReview{}
|
||||
for _, prr := range pr.Reviews.Nodes {
|
||||
//Dont display pending reviews
|
||||
//Dont display commenting reviews without top level comment body
|
||||
if prr.State != "PENDING" && !(prr.State == "COMMENTED" && prr.Body == "") {
|
||||
published = append(published, prr)
|
||||
}
|
||||
}
|
||||
return PullRequestReviews{Nodes: published, TotalCount: len(published)}
|
||||
}
|
||||
|
||||
func (c Client) PullRequestDiff(baseRepo ghrepo.Interface, prNumber int) (io.ReadCloser, error) {
|
||||
url := fmt.Sprintf("%srepos/%s/pulls/%d",
|
||||
ghinstance.RESTPrefix(baseRepo.RepoHost()), ghrepo.FullName(baseRepo), prNumber)
|
||||
|
|
@ -570,15 +570,6 @@ func PullRequestByNumber(client *Client, repo ghrepo.Interface, number int) (*Pu
|
|||
}
|
||||
totalCount
|
||||
}
|
||||
reviews(last: 100) {
|
||||
nodes {
|
||||
author {
|
||||
login
|
||||
}
|
||||
state
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
assignees(first: 100) {
|
||||
nodes {
|
||||
login
|
||||
|
|
@ -605,30 +596,8 @@ func PullRequestByNumber(client *Client, repo ghrepo.Interface, number int) (*Pu
|
|||
milestone{
|
||||
title
|
||||
}
|
||||
comments(last: 1) {
|
||||
nodes {
|
||||
author {
|
||||
login
|
||||
}
|
||||
authorAssociation
|
||||
body
|
||||
createdAt
|
||||
includesCreatedEdit
|
||||
reactionGroups {
|
||||
content
|
||||
users {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
reactionGroups {
|
||||
content
|
||||
users {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
` + commentsFragment() + `
|
||||
` + reactionGroupsFragment() + `
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
|
@ -703,15 +672,6 @@ func PullRequestForBranch(client *Client, repo ghrepo.Interface, baseBranch, hea
|
|||
}
|
||||
totalCount
|
||||
}
|
||||
reviews(last: 100) {
|
||||
nodes {
|
||||
author {
|
||||
login
|
||||
}
|
||||
state
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
assignees(first: 100) {
|
||||
nodes {
|
||||
login
|
||||
|
|
@ -738,30 +698,8 @@ func PullRequestForBranch(client *Client, repo ghrepo.Interface, baseBranch, hea
|
|||
milestone{
|
||||
title
|
||||
}
|
||||
comments(last: 1) {
|
||||
nodes {
|
||||
author {
|
||||
login
|
||||
}
|
||||
authorAssociation
|
||||
body
|
||||
createdAt
|
||||
includesCreatedEdit
|
||||
reactionGroups {
|
||||
content
|
||||
users {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
reactionGroups {
|
||||
content
|
||||
users {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
` + commentsFragment() + `
|
||||
` + reactionGroupsFragment() + `
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -821,7 +759,7 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter
|
|||
}
|
||||
for key, val := range params {
|
||||
switch key {
|
||||
case "title", "body", "draft", "baseRefName", "headRefName":
|
||||
case "title", "body", "draft", "baseRefName", "headRefName", "maintainerCanModify":
|
||||
inputParams[key] = val
|
||||
}
|
||||
}
|
||||
|
|
@ -906,34 +844,6 @@ func isBlank(v interface{}) bool {
|
|||
}
|
||||
}
|
||||
|
||||
func AddReview(client *Client, repo ghrepo.Interface, pr *PullRequest, input *PullRequestReviewInput) error {
|
||||
var mutation struct {
|
||||
AddPullRequestReview struct {
|
||||
ClientMutationID string
|
||||
} `graphql:"addPullRequestReview(input:$input)"`
|
||||
}
|
||||
|
||||
state := githubv4.PullRequestReviewEventComment
|
||||
switch input.State {
|
||||
case ReviewApprove:
|
||||
state = githubv4.PullRequestReviewEventApprove
|
||||
case ReviewRequestChanges:
|
||||
state = githubv4.PullRequestReviewEventRequestChanges
|
||||
}
|
||||
|
||||
body := githubv4.String(input.Body)
|
||||
variables := map[string]interface{}{
|
||||
"input": githubv4.AddPullRequestReviewInput{
|
||||
PullRequestID: pr.ID,
|
||||
Event: &state,
|
||||
Body: &body,
|
||||
},
|
||||
}
|
||||
|
||||
gql := graphQLClient(client.http, repo.RepoHost())
|
||||
return gql.MutateNamed(context.Background(), "PullRequestReviewAdd", &mutation, variables)
|
||||
}
|
||||
|
||||
func PullRequestList(client *Client, repo ghrepo.Interface, vars map[string]interface{}, limit int) (*PullRequestAndTotalCount, error) {
|
||||
type prBlock struct {
|
||||
Edges []struct {
|
||||
|
|
@ -1144,7 +1054,7 @@ func PullRequestReopen(client *Client, repo ghrepo.Interface, pr *PullRequest) e
|
|||
return err
|
||||
}
|
||||
|
||||
func PullRequestMerge(client *Client, repo ghrepo.Interface, pr *PullRequest, m PullRequestMergeMethod) error {
|
||||
func PullRequestMerge(client *Client, repo ghrepo.Interface, pr *PullRequest, m PullRequestMergeMethod, body *string) error {
|
||||
mergeMethod := githubv4.PullRequestMergeMethodMerge
|
||||
switch m {
|
||||
case PullRequestMergeMethodRebase:
|
||||
|
|
@ -1170,6 +1080,10 @@ func PullRequestMerge(client *Client, repo ghrepo.Interface, pr *PullRequest, m
|
|||
commitHeadline := githubv4.String(fmt.Sprintf("%s (#%d)", pr.Title, pr.Number))
|
||||
input.CommitHeadline = &commitHeadline
|
||||
}
|
||||
if body != nil {
|
||||
commitBody := githubv4.String(*body)
|
||||
input.CommitBody = &commitBody
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"input": input,
|
||||
|
|
|
|||
135
api/queries_pr_review.go
Normal file
135
api/queries_pr_review.go
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/shurcooL/githubv4"
|
||||
)
|
||||
|
||||
type PullRequestReviewState int
|
||||
|
||||
const (
|
||||
ReviewApprove PullRequestReviewState = iota
|
||||
ReviewRequestChanges
|
||||
ReviewComment
|
||||
)
|
||||
|
||||
type PullRequestReviewInput struct {
|
||||
Body string
|
||||
State PullRequestReviewState
|
||||
}
|
||||
|
||||
type PullRequestReviews struct {
|
||||
Nodes []PullRequestReview
|
||||
PageInfo PageInfo
|
||||
TotalCount int
|
||||
}
|
||||
|
||||
type PullRequestReview struct {
|
||||
Author Author
|
||||
AuthorAssociation string
|
||||
Body string
|
||||
CreatedAt time.Time
|
||||
IncludesCreatedEdit bool
|
||||
ReactionGroups ReactionGroups
|
||||
State string
|
||||
URL string
|
||||
}
|
||||
|
||||
func AddReview(client *Client, repo ghrepo.Interface, pr *PullRequest, input *PullRequestReviewInput) error {
|
||||
var mutation struct {
|
||||
AddPullRequestReview struct {
|
||||
ClientMutationID string
|
||||
} `graphql:"addPullRequestReview(input:$input)"`
|
||||
}
|
||||
|
||||
state := githubv4.PullRequestReviewEventComment
|
||||
switch input.State {
|
||||
case ReviewApprove:
|
||||
state = githubv4.PullRequestReviewEventApprove
|
||||
case ReviewRequestChanges:
|
||||
state = githubv4.PullRequestReviewEventRequestChanges
|
||||
}
|
||||
|
||||
body := githubv4.String(input.Body)
|
||||
variables := map[string]interface{}{
|
||||
"input": githubv4.AddPullRequestReviewInput{
|
||||
PullRequestID: pr.ID,
|
||||
Event: &state,
|
||||
Body: &body,
|
||||
},
|
||||
}
|
||||
|
||||
gql := graphQLClient(client.http, repo.RepoHost())
|
||||
return gql.MutateNamed(context.Background(), "PullRequestReviewAdd", &mutation, variables)
|
||||
}
|
||||
|
||||
func ReviewsForPullRequest(client *Client, repo ghrepo.Interface, pr *PullRequest) (*PullRequestReviews, error) {
|
||||
type response struct {
|
||||
Repository struct {
|
||||
PullRequest struct {
|
||||
Reviews PullRequestReviews `graphql:"reviews(first: 100, after: $endCursor)"`
|
||||
} `graphql:"pullRequest(number: $number)"`
|
||||
} `graphql:"repository(owner: $owner, name: $repo)"`
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"owner": githubv4.String(repo.RepoOwner()),
|
||||
"repo": githubv4.String(repo.RepoName()),
|
||||
"number": githubv4.Int(pr.Number),
|
||||
"endCursor": (*githubv4.String)(nil),
|
||||
}
|
||||
|
||||
gql := graphQLClient(client.http, repo.RepoHost())
|
||||
|
||||
var reviews []PullRequestReview
|
||||
for {
|
||||
var query response
|
||||
err := gql.QueryNamed(context.Background(), "ReviewsForPullRequest", &query, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reviews = append(reviews, query.Repository.PullRequest.Reviews.Nodes...)
|
||||
if !query.Repository.PullRequest.Reviews.PageInfo.HasNextPage {
|
||||
break
|
||||
}
|
||||
variables["endCursor"] = githubv4.String(query.Repository.PullRequest.Reviews.PageInfo.EndCursor)
|
||||
}
|
||||
|
||||
return &PullRequestReviews{Nodes: reviews, TotalCount: len(reviews)}, nil
|
||||
}
|
||||
|
||||
func (prr PullRequestReview) AuthorLogin() string {
|
||||
return prr.Author.Login
|
||||
}
|
||||
|
||||
func (prr PullRequestReview) Association() string {
|
||||
return prr.AuthorAssociation
|
||||
}
|
||||
|
||||
func (prr PullRequestReview) Content() string {
|
||||
return prr.Body
|
||||
}
|
||||
|
||||
func (prr PullRequestReview) Created() time.Time {
|
||||
return prr.CreatedAt
|
||||
}
|
||||
|
||||
func (prr PullRequestReview) IsEdited() bool {
|
||||
return prr.IncludesCreatedEdit
|
||||
}
|
||||
|
||||
func (prr PullRequestReview) Reactions() ReactionGroups {
|
||||
return prr.ReactionGroups
|
||||
}
|
||||
|
||||
func (prr PullRequestReview) Status() string {
|
||||
return prr.State
|
||||
}
|
||||
|
||||
func (prr PullRequestReview) Link() string {
|
||||
return prr.URL
|
||||
}
|
||||
|
|
@ -33,6 +33,10 @@ type Repository struct {
|
|||
|
||||
Parent *Repository
|
||||
|
||||
MergeCommitAllowed bool
|
||||
RebaseMergeAllowed bool
|
||||
SquashMergeAllowed bool
|
||||
|
||||
// pseudo-field that keeps track of host name of this repo
|
||||
hostname string
|
||||
}
|
||||
|
|
@ -108,6 +112,9 @@ func GitHubRepo(client *Client, repo ghrepo.Interface) (*Repository, error) {
|
|||
parent {
|
||||
...repo
|
||||
}
|
||||
mergeCommitAllowed
|
||||
rebaseMergeAllowed
|
||||
squashMergeAllowed
|
||||
}
|
||||
}`
|
||||
variables := map[string]interface{}{
|
||||
|
|
@ -457,6 +464,33 @@ func (m *RepoMetadataResult) ProjectsToIDs(names []string) ([]string, error) {
|
|||
return ids, nil
|
||||
}
|
||||
|
||||
func ProjectsToPaths(projects []RepoProject, names []string) ([]string, error) {
|
||||
var paths []string
|
||||
for _, projectName := range names {
|
||||
found := false
|
||||
for _, p := range projects {
|
||||
if strings.EqualFold(projectName, p.Name) {
|
||||
// format of ResourcePath: /OWNER/REPO/projects/PROJECT_NUMBER or /orgs/ORG/projects/PROJECT_NUMBER
|
||||
// required format of path: OWNER/REPO/PROJECT_NUMBER or ORG/PROJECT_NUMBER
|
||||
var path string
|
||||
pathParts := strings.Split(p.ResourcePath, "/")
|
||||
if pathParts[1] == "orgs" {
|
||||
path = fmt.Sprintf("%s/%s", pathParts[2], pathParts[4])
|
||||
} else {
|
||||
path = fmt.Sprintf("%s/%s/%s", pathParts[1], pathParts[2], pathParts[4])
|
||||
}
|
||||
paths = append(paths, path)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("'%s' not found", projectName)
|
||||
}
|
||||
}
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
func (m *RepoMetadataResult) MilestoneToID(title string) (string, error) {
|
||||
for _, m := range m.Milestones {
|
||||
if strings.EqualFold(title, m.Title) {
|
||||
|
|
@ -540,20 +574,12 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput
|
|||
if input.Projects {
|
||||
count++
|
||||
go func() {
|
||||
projects, err := RepoProjects(client, repo)
|
||||
projects, err := RepoAndOrgProjects(client, repo)
|
||||
if err != nil {
|
||||
errc <- fmt.Errorf("error fetching projects: %w", err)
|
||||
errc <- err
|
||||
return
|
||||
}
|
||||
result.Projects = projects
|
||||
|
||||
orgProjects, err := OrganizationProjects(client, repo)
|
||||
// TODO: better detection of non-org repos
|
||||
if err != nil && !strings.HasPrefix(err.Error(), "Could not resolve to an Organization") {
|
||||
errc <- fmt.Errorf("error fetching organization projects: %w", err)
|
||||
return
|
||||
}
|
||||
result.Projects = append(result.Projects, orgProjects...)
|
||||
errc <- nil
|
||||
}()
|
||||
}
|
||||
|
|
@ -682,8 +708,9 @@ func RepoResolveMetadataIDs(client *Client, repo ghrepo.Interface, input RepoRes
|
|||
}
|
||||
|
||||
type RepoProject struct {
|
||||
ID string
|
||||
Name string
|
||||
ID string
|
||||
Name string
|
||||
ResourcePath string
|
||||
}
|
||||
|
||||
// RepoProjects fetches all open projects for a repository
|
||||
|
|
@ -726,6 +753,23 @@ func RepoProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, error)
|
|||
return projects, nil
|
||||
}
|
||||
|
||||
// RepoAndOrgProjects fetches all open projects for a repository and its org
|
||||
func RepoAndOrgProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, error) {
|
||||
projects, err := RepoProjects(client, repo)
|
||||
if err != nil {
|
||||
return projects, fmt.Errorf("error fetching projects: %w", err)
|
||||
}
|
||||
|
||||
orgProjects, err := OrganizationProjects(client, repo)
|
||||
// TODO: better detection of non-org repos
|
||||
if err != nil && !strings.HasPrefix(err.Error(), "Could not resolve to an Organization") {
|
||||
return projects, fmt.Errorf("error fetching organization projects: %w", err)
|
||||
}
|
||||
projects = append(projects, orgProjects...)
|
||||
|
||||
return projects, nil
|
||||
}
|
||||
|
||||
type RepoAssignee struct {
|
||||
ID string
|
||||
Login string
|
||||
|
|
@ -913,3 +957,12 @@ func MilestoneByNumber(client *Client, repo ghrepo.Interface, number int32) (*Re
|
|||
|
||||
return query.Repository.Milestone, nil
|
||||
}
|
||||
|
||||
func ProjectNamesToPaths(client *Client, repo ghrepo.Interface, projectNames []string) ([]string, error) {
|
||||
var paths []string
|
||||
projects, err := RepoAndOrgProjects(client, repo)
|
||||
if err != nil {
|
||||
return paths, err
|
||||
}
|
||||
return ProjectsToPaths(projects, projectNames)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,6 +141,63 @@ func Test_RepoMetadata(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_ProjectsToPaths(t *testing.T) {
|
||||
expectedProjectPaths := []string{"OWNER/REPO/PROJECT_NUMBER", "ORG/PROJECT_NUMBER"}
|
||||
projects := []RepoProject{
|
||||
{"id1", "My Project", "/OWNER/REPO/projects/PROJECT_NUMBER"},
|
||||
{"id2", "Org Project", "/orgs/ORG/projects/PROJECT_NUMBER"},
|
||||
{"id3", "Project", "/orgs/ORG/projects/PROJECT_NUMBER_2"},
|
||||
}
|
||||
projectNames := []string{"My Project", "Org Project"}
|
||||
|
||||
projectPaths, err := ProjectsToPaths(projects, projectNames)
|
||||
if err != nil {
|
||||
t.Errorf("error resolving projects: %v", err)
|
||||
}
|
||||
if !sliceEqual(projectPaths, expectedProjectPaths) {
|
||||
t.Errorf("expected projects %v, got %v", expectedProjectPaths, projectPaths)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ProjectNamesToPaths(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
client := NewClient(ReplaceTripper(http))
|
||||
|
||||
repo, _ := ghrepo.FromFullName("OWNER/REPO")
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query RepositoryProjectList\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "projects": {
|
||||
"nodes": [
|
||||
{ "name": "Cleanup", "id": "CLEANUPID", "resourcePath": "/OWNER/REPO/projects/1" },
|
||||
{ "name": "Roadmap", "id": "ROADMAPID", "resourcePath": "/OWNER/REPO/projects/2" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query OrganizationProjectList\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "organization": { "projects": {
|
||||
"nodes": [
|
||||
{ "name": "Triage", "id": "TRIAGEID", "resourcePath": "/orgs/ORG/projects/1" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
|
||||
projectPaths, err := ProjectNamesToPaths(client, repo, []string{"Triage", "Roadmap"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
expectedProjectPaths := []string{"ORG/1", "OWNER/REPO/2"}
|
||||
if !sliceEqual(projectPaths, expectedProjectPaths) {
|
||||
t.Errorf("expected projects paths %v, got %v", expectedProjectPaths, projectPaths)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_RepoResolveMetadataIDs(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
client := NewClient(ReplaceTripper(http))
|
||||
|
|
|
|||
|
|
@ -29,3 +29,12 @@ var reactionEmoji = map[string]string{
|
|||
"ROCKET": "\U0001f680",
|
||||
"EYES": "\U0001f440",
|
||||
}
|
||||
|
||||
func reactionGroupsFragment() string {
|
||||
return `reactionGroups {
|
||||
content
|
||||
users {
|
||||
totalCount
|
||||
}
|
||||
}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,14 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func eq(t *testing.T, got interface{}, expected interface{}) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(got, expected) {
|
||||
t.Errorf("expected: %v, got: %v", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Remotes_FindByName(t *testing.T) {
|
||||
list := Remotes{
|
||||
&Remote{Remote: &git.Remote{Name: "mona"}, Repo: ghrepo.New("monalisa", "myfork")},
|
||||
|
|
@ -25,15 +17,15 @@ func Test_Remotes_FindByName(t *testing.T) {
|
|||
}
|
||||
|
||||
r, err := list.FindByName("upstream", "origin")
|
||||
eq(t, err, nil)
|
||||
eq(t, r.Name, "upstream")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "upstream", r.Name)
|
||||
|
||||
r, err = list.FindByName("nonexistent", "*")
|
||||
eq(t, err, nil)
|
||||
eq(t, r.Name, "mona")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "mona", r.Name)
|
||||
|
||||
_, err = list.FindByName("nonexistent")
|
||||
eq(t, err, errors.New(`no GitHub remotes found`))
|
||||
assert.Error(t, err, "no GitHub remotes found")
|
||||
}
|
||||
|
||||
func Test_translateRemotes(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -22,18 +22,18 @@
|
|||
# installs to '/usr/local' by default; sudo may be required
|
||||
$ make install
|
||||
|
||||
# install to a different location
|
||||
# or, install to a different location
|
||||
$ make install prefix=/path/to/gh
|
||||
```
|
||||
|
||||
#### Windows
|
||||
```sh
|
||||
# build the binary
|
||||
> go build -o gh.exe ./cmd/gh
|
||||
# build the `bin\gh.exe` binary
|
||||
> go run script/build.go
|
||||
```
|
||||
There is no install step available on Windows.
|
||||
|
||||
3. Run `gh version` to check if it worked.
|
||||
|
||||
#### Windows
|
||||
Run `.\gh version` to check if it worked.
|
||||
Run `bin\gh version` to check if it worked.
|
||||
|
|
|
|||
24
git/git.go
24
git/git.go
|
|
@ -67,22 +67,21 @@ func CurrentBranch() (string, error) {
|
|||
return "", err
|
||||
}
|
||||
|
||||
stderr := bytes.Buffer{}
|
||||
refCmd.Stderr = &stderr
|
||||
|
||||
output, err := run.PrepareCmd(refCmd).Output()
|
||||
if err == nil {
|
||||
// Found the branch name
|
||||
return getBranchShortName(output), nil
|
||||
}
|
||||
|
||||
var cmdErr *run.CmdError
|
||||
if errors.As(err, &cmdErr) {
|
||||
if cmdErr.Stderr.Len() == 0 {
|
||||
// Detached head
|
||||
return "", ErrNotOnAnyBranch
|
||||
}
|
||||
if stderr.Len() == 0 {
|
||||
// Detached head
|
||||
return "", ErrNotOnAnyBranch
|
||||
}
|
||||
|
||||
// Unknown error
|
||||
return "", err
|
||||
return "", fmt.Errorf("%sgit: %s", stderr.String(), err)
|
||||
}
|
||||
|
||||
func listRemotes() ([]string, error) {
|
||||
|
|
@ -308,8 +307,13 @@ func RunClone(cloneURL string, args []string) (target string, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func AddUpstreamRemote(upstreamURL, cloneDir string) error {
|
||||
cloneCmd, err := GitCommand("-C", cloneDir, "remote", "add", "-f", "upstream", upstreamURL)
|
||||
func AddUpstreamRemote(upstreamURL, cloneDir string, branches []string) error {
|
||||
args := []string{"-C", cloneDir, "remote", "add"}
|
||||
for _, branch := range branches {
|
||||
args = append(args, "-t", branch)
|
||||
}
|
||||
args = append(args, "-f", "upstream", upstreamURL)
|
||||
cloneCmd, err := GitCommand(args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
103
git/git_test.go
103
git/git_test.go
|
|
@ -1,12 +1,10 @@
|
|||
package git
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/internal/run"
|
||||
"github.com/cli/cli/test"
|
||||
)
|
||||
|
||||
func Test_UncommittedChangeCount(t *testing.T) {
|
||||
|
|
@ -21,20 +19,17 @@ func Test_UncommittedChangeCount(t *testing.T) {
|
|||
{Label: "untracked file", Expected: 2, Output: " M poem.txt\n?? new.txt"},
|
||||
}
|
||||
|
||||
teardown := run.SetPrepareCmd(func(*exec.Cmd) run.Runnable {
|
||||
return &test.OutputStub{}
|
||||
})
|
||||
defer teardown()
|
||||
|
||||
for _, v := range cases {
|
||||
_ = run.SetPrepareCmd(func(*exec.Cmd) run.Runnable {
|
||||
return &test.OutputStub{Out: []byte(v.Output)}
|
||||
})
|
||||
ucc, _ := UncommittedChangeCount()
|
||||
t.Run(v.Label, func(t *testing.T) {
|
||||
cs, restore := run.Stub()
|
||||
defer restore(t)
|
||||
cs.Register(`git status --porcelain`, 0, v.Output)
|
||||
|
||||
if ucc != v.Expected {
|
||||
t.Errorf("got unexpected ucc value: %d for case %s", ucc, v.Label)
|
||||
}
|
||||
ucc, _ := UncommittedChangeCount()
|
||||
if ucc != v.Expected {
|
||||
t.Errorf("UncommittedChangeCount() = %d, expected %d", ucc, v.Expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -59,59 +54,32 @@ func Test_CurrentBranch(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, v := range cases {
|
||||
cs, teardown := test.InitCmdStubber()
|
||||
cs.Stub(v.Stub)
|
||||
cs, teardown := run.Stub()
|
||||
cs.Register(`git symbolic-ref --quiet HEAD`, 0, v.Stub)
|
||||
|
||||
result, err := CurrentBranch()
|
||||
if err != nil {
|
||||
t.Errorf("got unexpected error: %w", err)
|
||||
}
|
||||
if len(cs.Calls) != 1 {
|
||||
t.Errorf("expected 1 git call, saw %d", len(cs.Calls))
|
||||
}
|
||||
if result != v.Expected {
|
||||
t.Errorf("unexpected branch name: %s instead of %s", result, v.Expected)
|
||||
}
|
||||
teardown()
|
||||
teardown(t)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CurrentBranch_detached_head(t *testing.T) {
|
||||
cs, teardown := test.InitCmdStubber()
|
||||
defer teardown()
|
||||
|
||||
cs.StubError("")
|
||||
cs, teardown := run.Stub()
|
||||
defer teardown(t)
|
||||
cs.Register(`git symbolic-ref --quiet HEAD`, 1, "")
|
||||
|
||||
_, err := CurrentBranch()
|
||||
if err == nil {
|
||||
t.Errorf("expected an error")
|
||||
t.Fatal("expected an error, got nil")
|
||||
}
|
||||
if err != ErrNotOnAnyBranch {
|
||||
t.Errorf("got unexpected error: %s instead of %s", err, ErrNotOnAnyBranch)
|
||||
}
|
||||
if len(cs.Calls) != 1 {
|
||||
t.Errorf("expected 1 git call, saw %d", len(cs.Calls))
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CurrentBranch_unexpected_error(t *testing.T) {
|
||||
cs, teardown := test.InitCmdStubber()
|
||||
defer teardown()
|
||||
|
||||
cs.StubError("lol")
|
||||
|
||||
expectedError := "lol\nstub: lol"
|
||||
|
||||
_, err := CurrentBranch()
|
||||
if err == nil {
|
||||
t.Errorf("expected an error")
|
||||
}
|
||||
if err.Error() != expectedError {
|
||||
t.Errorf("got unexpected error: %s instead of %s", err.Error(), expectedError)
|
||||
}
|
||||
if len(cs.Calls) != 1 {
|
||||
t.Errorf("expected 1 git call, saw %d", len(cs.Calls))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseExtraCloneArgs(t *testing.T) {
|
||||
|
|
@ -170,5 +138,42 @@ func TestParseExtraCloneArgs(t *testing.T) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAddUpstreamRemote(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
upstreamURL string
|
||||
cloneDir string
|
||||
branches []string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "fetch all",
|
||||
upstreamURL: "URL",
|
||||
cloneDir: "DIRECTORY",
|
||||
branches: []string{},
|
||||
want: "git -C DIRECTORY remote add -f upstream URL",
|
||||
},
|
||||
{
|
||||
name: "fetch specific branches only",
|
||||
upstreamURL: "URL",
|
||||
cloneDir: "DIRECTORY",
|
||||
branches: []string{"master", "dev"},
|
||||
want: "git -C DIRECTORY remote add -t master -t dev -f upstream URL",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
cs.Register(tt.want, 0, "")
|
||||
|
||||
err := AddUpstreamRemote(tt.upstreamURL, tt.cloneDir, tt.branches)
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `git remote add -f`: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,10 @@
|
|||
package git
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TODO: extract assertion helpers into a shared package
|
||||
func eq(t *testing.T, got interface{}, expected interface{}) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(got, expected) {
|
||||
t.Errorf("expected: %v, got: %v", expected, got)
|
||||
}
|
||||
}
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_parseRemotes(t *testing.T) {
|
||||
remoteList := []string{
|
||||
|
|
@ -23,20 +16,20 @@ func Test_parseRemotes(t *testing.T) {
|
|||
"zardoz\thttps://example.com/zed.git (push)",
|
||||
}
|
||||
r := parseRemotes(remoteList)
|
||||
eq(t, len(r), 4)
|
||||
assert.Equal(t, 4, len(r))
|
||||
|
||||
eq(t, r[0].Name, "mona")
|
||||
eq(t, r[0].FetchURL.String(), "ssh://git@github.com/monalisa/myfork.git")
|
||||
assert.Equal(t, "mona", r[0].Name)
|
||||
assert.Equal(t, "ssh://git@github.com/monalisa/myfork.git", r[0].FetchURL.String())
|
||||
if r[0].PushURL != nil {
|
||||
t.Errorf("expected no PushURL, got %q", r[0].PushURL)
|
||||
}
|
||||
eq(t, r[1].Name, "origin")
|
||||
eq(t, r[1].FetchURL.Path, "/monalisa/octo-cat.git")
|
||||
eq(t, r[1].PushURL.Path, "/monalisa/octo-cat-push.git")
|
||||
assert.Equal(t, "origin", r[1].Name)
|
||||
assert.Equal(t, "/monalisa/octo-cat.git", r[1].FetchURL.Path)
|
||||
assert.Equal(t, "/monalisa/octo-cat-push.git", r[1].PushURL.Path)
|
||||
|
||||
eq(t, r[2].Name, "upstream")
|
||||
eq(t, r[2].FetchURL.Host, "example.com")
|
||||
eq(t, r[2].PushURL.Host, "github.com")
|
||||
assert.Equal(t, "upstream", r[2].Name)
|
||||
assert.Equal(t, "example.com", r[2].FetchURL.Host)
|
||||
assert.Equal(t, "github.com", r[2].PushURL.Host)
|
||||
|
||||
eq(t, r[3].Name, "zardoz")
|
||||
assert.Equal(t, "zardoz", r[3].Name)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/cli/cli/internal/config"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -147,10 +147,10 @@ func ParseSSHConfig() SSHAliasMap {
|
|||
|
||||
p := sshParser{}
|
||||
|
||||
if homedir, err := homedir.Dir(); err == nil {
|
||||
userConfig := filepath.Join(homedir, ".ssh", "config")
|
||||
if sshDir, err := config.HomeDirPath(".ssh"); err == nil {
|
||||
userConfig := filepath.Join(sshDir, "config")
|
||||
configFiles = append([]string{userConfig}, configFiles...)
|
||||
p.homeDir = homedir
|
||||
p.homeDir = filepath.Dir(sshDir)
|
||||
}
|
||||
|
||||
for _, file := range configFiles {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
|
|
@ -14,8 +15,8 @@ import (
|
|||
)
|
||||
|
||||
func ConfigDir() string {
|
||||
dir, _ := homedir.Expand("~/.config/gh")
|
||||
return dir
|
||||
homeDir, _ := homeDirAutoMigrate()
|
||||
return homeDir
|
||||
}
|
||||
|
||||
func ConfigFile() string {
|
||||
|
|
@ -30,6 +31,62 @@ func ParseDefaultConfig() (Config, error) {
|
|||
return ParseConfig(ConfigFile())
|
||||
}
|
||||
|
||||
func HomeDirPath(subdir string) (string, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
// TODO: remove go-homedir fallback in GitHub CLI v2
|
||||
if legacyDir, err := homedir.Dir(); err == nil {
|
||||
return filepath.Join(legacyDir, subdir), nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
newPath := filepath.Join(homeDir, subdir)
|
||||
if s, err := os.Stat(newPath); err == nil && s.IsDir() {
|
||||
return newPath, nil
|
||||
}
|
||||
|
||||
// TODO: remove go-homedir fallback in GitHub CLI v2
|
||||
if legacyDir, err := homedir.Dir(); err == nil {
|
||||
legacyPath := filepath.Join(legacyDir, subdir)
|
||||
if s, err := os.Stat(legacyPath); err == nil && s.IsDir() {
|
||||
return legacyPath, nil
|
||||
}
|
||||
}
|
||||
|
||||
return newPath, nil
|
||||
}
|
||||
|
||||
// Looks up the `~/.config/gh` directory with backwards-compatibility with go-homedir and auto-migration
|
||||
// when an old homedir location was found.
|
||||
func homeDirAutoMigrate() (string, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
// TODO: remove go-homedir fallback in GitHub CLI v2
|
||||
if legacyDir, err := homedir.Dir(); err == nil {
|
||||
return filepath.Join(legacyDir, ".config", "gh"), nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
newPath := filepath.Join(homeDir, ".config", "gh")
|
||||
_, newPathErr := os.Stat(newPath)
|
||||
if newPathErr == nil || !os.IsNotExist(err) {
|
||||
return newPath, newPathErr
|
||||
}
|
||||
|
||||
// TODO: remove go-homedir fallback in GitHub CLI v2
|
||||
if legacyDir, err := homedir.Dir(); err == nil {
|
||||
legacyPath := filepath.Join(legacyDir, ".config", "gh")
|
||||
if s, err := os.Stat(legacyPath); err == nil && s.IsDir() {
|
||||
_ = os.MkdirAll(filepath.Dir(newPath), 0755)
|
||||
return newPath, os.Rename(legacyPath, newPath)
|
||||
}
|
||||
}
|
||||
|
||||
return newPath, nil
|
||||
}
|
||||
|
||||
var ReadConfigFile = func(filename string) ([]byte, error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -3,20 +3,12 @@ package config
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func eq(t *testing.T, got interface{}, expected interface{}) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(got, expected) {
|
||||
t.Errorf("expected: %v, got: %v", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseConfig(t *testing.T) {
|
||||
defer StubConfig(`---
|
||||
hosts:
|
||||
|
|
@ -25,13 +17,13 @@ hosts:
|
|||
oauth_token: OTOKEN
|
||||
`, "")()
|
||||
config, err := ParseConfig("config.yml")
|
||||
eq(t, err, nil)
|
||||
assert.NoError(t, err)
|
||||
user, err := config.Get("github.com", "user")
|
||||
eq(t, err, nil)
|
||||
eq(t, user, "monalisa")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "monalisa", user)
|
||||
token, err := config.Get("github.com", "oauth_token")
|
||||
eq(t, err, nil)
|
||||
eq(t, token, "OTOKEN")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "OTOKEN", token)
|
||||
}
|
||||
|
||||
func Test_parseConfig_multipleHosts(t *testing.T) {
|
||||
|
|
@ -45,13 +37,13 @@ hosts:
|
|||
oauth_token: OTOKEN
|
||||
`, "")()
|
||||
config, err := ParseConfig("config.yml")
|
||||
eq(t, err, nil)
|
||||
assert.NoError(t, err)
|
||||
user, err := config.Get("github.com", "user")
|
||||
eq(t, err, nil)
|
||||
eq(t, user, "monalisa")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "monalisa", user)
|
||||
token, err := config.Get("github.com", "oauth_token")
|
||||
eq(t, err, nil)
|
||||
eq(t, token, "OTOKEN")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "OTOKEN", token)
|
||||
}
|
||||
|
||||
func Test_parseConfig_hostsFile(t *testing.T) {
|
||||
|
|
@ -61,13 +53,13 @@ github.com:
|
|||
oauth_token: OTOKEN
|
||||
`)()
|
||||
config, err := ParseConfig("config.yml")
|
||||
eq(t, err, nil)
|
||||
assert.NoError(t, err)
|
||||
user, err := config.Get("github.com", "user")
|
||||
eq(t, err, nil)
|
||||
eq(t, user, "monalisa")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "monalisa", user)
|
||||
token, err := config.Get("github.com", "oauth_token")
|
||||
eq(t, err, nil)
|
||||
eq(t, token, "OTOKEN")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "OTOKEN", token)
|
||||
}
|
||||
|
||||
func Test_parseConfig_hostFallback(t *testing.T) {
|
||||
|
|
@ -83,16 +75,16 @@ example.com:
|
|||
git_protocol: https
|
||||
`)()
|
||||
config, err := ParseConfig("config.yml")
|
||||
eq(t, err, nil)
|
||||
assert.NoError(t, err)
|
||||
val, err := config.Get("example.com", "git_protocol")
|
||||
eq(t, err, nil)
|
||||
eq(t, val, "https")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https", val)
|
||||
val, err = config.Get("github.com", "git_protocol")
|
||||
eq(t, err, nil)
|
||||
eq(t, val, "ssh")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "ssh", val)
|
||||
val, err = config.Get("nonexistent.io", "git_protocol")
|
||||
eq(t, err, nil)
|
||||
eq(t, val, "ssh")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "ssh", val)
|
||||
}
|
||||
|
||||
func Test_ParseConfig_migrateConfig(t *testing.T) {
|
||||
|
|
@ -108,7 +100,7 @@ github.com:
|
|||
defer StubBackupConfig()()
|
||||
|
||||
_, err := ParseConfig("config.yml")
|
||||
assert.Nil(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedHosts := `github.com:
|
||||
user: keiyuri
|
||||
|
|
|
|||
|
|
@ -55,15 +55,15 @@ func Test_defaultConfig(t *testing.T) {
|
|||
assert.Equal(t, "", hostsBuf.String())
|
||||
|
||||
proto, err := cfg.Get("", "git_protocol")
|
||||
assert.Nil(t, err)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https", proto)
|
||||
|
||||
editor, err := cfg.Get("", "editor")
|
||||
assert.Nil(t, err)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", editor)
|
||||
|
||||
aliases, err := cfg.Aliases()
|
||||
assert.Nil(t, err)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(aliases.All()), 1)
|
||||
expansion, _ := aliases.Get("co")
|
||||
assert.Equal(t, expansion, "pr checkout")
|
||||
|
|
@ -74,13 +74,13 @@ func Test_ValidateValue(t *testing.T) {
|
|||
assert.EqualError(t, err, "invalid value")
|
||||
|
||||
err = ValidateValue("git_protocol", "ssh")
|
||||
assert.Nil(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = ValidateValue("editor", "vim")
|
||||
assert.Nil(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = ValidateValue("got", "123")
|
||||
assert.Nil(t, err)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_ValidateKey(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,14 @@ const (
|
|||
GITHUB_ENTERPRISE_TOKEN = "GITHUB_ENTERPRISE_TOKEN"
|
||||
)
|
||||
|
||||
type ReadOnlyEnvError struct {
|
||||
Variable string
|
||||
}
|
||||
|
||||
func (e *ReadOnlyEnvError) Error() string {
|
||||
return fmt.Sprintf("read-only value in %s", e.Variable)
|
||||
}
|
||||
|
||||
func InheritEnv(c Config) Config {
|
||||
return &envConfig{Config: c}
|
||||
}
|
||||
|
|
@ -56,7 +64,7 @@ func (c *envConfig) GetWithSource(hostname, key string) (string, string, error)
|
|||
func (c *envConfig) CheckWriteable(hostname, key string) error {
|
||||
if hostname != "" && key == "oauth_token" {
|
||||
if token, env := AuthTokenFromEnv(hostname); token != "" {
|
||||
return fmt.Errorf("read-only token in %s cannot be modified", env)
|
||||
return &ReadOnlyEnvError{Variable: env}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -279,7 +279,9 @@ func TestInheritEnv(t *testing.T) {
|
|||
assert.Equal(t, tt.wants.token, val)
|
||||
|
||||
err := cfg.CheckWriteable(tt.hostname, "oauth_token")
|
||||
assert.Equal(t, tt.wants.writeable, err == nil)
|
||||
if tt.wants.writeable != (err == nil) {
|
||||
t.Errorf("CheckWriteable() = %v, wants %v", err, tt.wants.writeable)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ func TestHostnameValidator(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.Equal(t, nil, err)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ var PrepareCmd = func(cmd *exec.Cmd) Runnable {
|
|||
return &cmdWithStderr{cmd}
|
||||
}
|
||||
|
||||
// SetPrepareCmd overrides PrepareCmd and returns a func to revert it back
|
||||
// Deprecated: use Stub
|
||||
func SetPrepareCmd(fn func(*exec.Cmd) Runnable) func() {
|
||||
origPrepare := PrepareCmd
|
||||
PrepareCmd = func(cmd *exec.Cmd) Runnable {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ package update
|
|||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
|
|
@ -11,6 +14,8 @@ import (
|
|||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var gitDescribeSuffixRE = regexp.MustCompile(`\d+-\d+-g[a-f0-9]{8}$`)
|
||||
|
||||
// ReleaseInfo stores information about a release
|
||||
type ReleaseInfo struct {
|
||||
Version string `json:"tag_name"`
|
||||
|
|
@ -83,6 +88,12 @@ func setStateEntry(stateFilePath string, t time.Time, r ReleaseInfo) error {
|
|||
}
|
||||
|
||||
func versionGreaterThan(v, w string) bool {
|
||||
w = gitDescribeSuffixRE.ReplaceAllStringFunc(w, func(m string) string {
|
||||
idx := strings.IndexRune(m, '-')
|
||||
n, _ := strconv.Atoi(m[0:idx])
|
||||
return fmt.Sprintf("%d-pre.0", n+1)
|
||||
})
|
||||
|
||||
vv, ve := version.NewVersion(v)
|
||||
vw, we := version.NewVersion(w)
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,27 @@ func TestCheckForUpdate(t *testing.T) {
|
|||
LatestURL: "https://www.spacejam.com/archive/spacejam/movie/jam.htm",
|
||||
ExpectsResult: true,
|
||||
},
|
||||
{
|
||||
Name: "current is built from source",
|
||||
CurrentVersion: "v1.2.3-123-gdeadbeef",
|
||||
LatestVersion: "v1.2.3",
|
||||
LatestURL: "https://www.spacejam.com/archive/spacejam/movie/jam.htm",
|
||||
ExpectsResult: false,
|
||||
},
|
||||
{
|
||||
Name: "current is built from source after a prerelease",
|
||||
CurrentVersion: "v1.2.3-rc.1-123-gdeadbeef",
|
||||
LatestVersion: "v1.2.3",
|
||||
LatestURL: "https://www.spacejam.com/archive/spacejam/movie/jam.htm",
|
||||
ExpectsResult: true,
|
||||
},
|
||||
{
|
||||
Name: "latest is newer than version build from source",
|
||||
CurrentVersion: "v1.2.3-123-gdeadbeef",
|
||||
LatestVersion: "v1.2.4",
|
||||
LatestURL: "https://www.spacejam.com/archive/spacejam/movie/jam.htm",
|
||||
ExpectsResult: true,
|
||||
},
|
||||
{
|
||||
Name: "latest is current",
|
||||
CurrentVersion: "v1.0.0",
|
||||
|
|
|
|||
|
|
@ -76,9 +76,7 @@ func TestAliasDelete(t *testing.T) {
|
|||
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantErr != "" {
|
||||
if assert.Error(t, err) {
|
||||
assert.Equal(t, tt.wantErr, err.Error())
|
||||
}
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
|
|||
#=> gh pr view -w 123
|
||||
|
||||
$ gh alias set bugs 'issue list --label="bugs"'
|
||||
$ gh bugs
|
||||
|
||||
$ gh alias set homework 'issue list --assigned @me'
|
||||
$ gh homework
|
||||
|
||||
$ gh alias set epicsBy 'issue list --author="$1" --label="epic"'
|
||||
$ gh epicsBy vilmibm
|
||||
|
|
|
|||
|
|
@ -65,10 +65,7 @@ func TestAliasSet_gh_command(t *testing.T) {
|
|||
cfg := config.NewFromString(``)
|
||||
|
||||
_, err := runCommand(cfg, true, "pr 'pr status'")
|
||||
|
||||
if assert.Error(t, err) {
|
||||
assert.Equal(t, `could not create alias: "pr" is already a gh command`, err.Error())
|
||||
}
|
||||
assert.EqualError(t, err, `could not create alias: "pr" is already a gh command`)
|
||||
}
|
||||
|
||||
func TestAliasSet_empty_aliases(t *testing.T) {
|
||||
|
|
@ -86,7 +83,9 @@ func TestAliasSet_empty_aliases(t *testing.T) {
|
|||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), "Added alias")
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.String(), "")
|
||||
|
||||
expected := `aliases:
|
||||
|
|
@ -108,6 +107,7 @@ func TestAliasSet_existing_alias(t *testing.T) {
|
|||
output, err := runCommand(cfg, true, "co 'pr checkout -Rcool/repo'")
|
||||
require.NoError(t, err)
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), "Changed alias.*co.*from.*pr checkout.*to.*pr checkout -Rcool/repo")
|
||||
}
|
||||
|
||||
|
|
@ -120,8 +120,10 @@ func TestAliasSet_space_args(t *testing.T) {
|
|||
output, err := runCommand(cfg, true, `il 'issue list -l "cool story"'`)
|
||||
require.NoError(t, err)
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), `Adding alias for.*il.*issue list -l "cool story"`)
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, mainBuf.String(), `il: issue list -l "cool story"`)
|
||||
}
|
||||
|
||||
|
|
@ -156,7 +158,9 @@ func TestAliasSet_arg_processing(t *testing.T) {
|
|||
t.Fatalf("got unexpected error running %s: %s", c.Cmd, err)
|
||||
}
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), c.ExpectedOutputLine)
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, mainBuf.String(), c.ExpectedConfigLine)
|
||||
})
|
||||
}
|
||||
|
|
@ -178,6 +182,7 @@ aliases:
|
|||
diff: pr diff
|
||||
`
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), "Adding alias for.*diff.*pr diff", "Added alias.")
|
||||
assert.Equal(t, expected, mainBuf.String())
|
||||
}
|
||||
|
|
@ -199,6 +204,7 @@ func TestAliasSet_existing_aliases(t *testing.T) {
|
|||
view: pr view
|
||||
`
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), "Adding alias for.*view.*pr view", "Added alias.")
|
||||
assert.Equal(t, expected, mainBuf.String())
|
||||
|
||||
|
|
@ -210,9 +216,7 @@ func TestAliasSet_invalid_command(t *testing.T) {
|
|||
cfg := config.NewFromString(``)
|
||||
|
||||
_, err := runCommand(cfg, true, "co 'pe checkout'")
|
||||
if assert.Error(t, err) {
|
||||
assert.Equal(t, "could not create alias: pe checkout does not correspond to a gh command", err.Error())
|
||||
}
|
||||
assert.EqualError(t, err, "could not create alias: pe checkout does not correspond to a gh command")
|
||||
}
|
||||
|
||||
func TestShellAlias_flag(t *testing.T) {
|
||||
|
|
@ -226,6 +230,7 @@ func TestShellAlias_flag(t *testing.T) {
|
|||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), "Adding alias for.*igrep")
|
||||
|
||||
expected := `aliases:
|
||||
|
|
@ -243,6 +248,7 @@ func TestShellAlias_bang(t *testing.T) {
|
|||
output, err := runCommand(cfg, true, "igrep '!gh issue list | grep'")
|
||||
require.NoError(t, err)
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), "Adding alias for.*igrep")
|
||||
|
||||
expected := `aliases:
|
||||
|
|
|
|||
|
|
@ -120,70 +120,56 @@ func loginRun(opts *LoginOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
hostname := opts.Hostname
|
||||
if hostname == "" {
|
||||
if opts.Interactive {
|
||||
var err error
|
||||
hostname, err = promptForHostname()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return errors.New("must specify --hostname")
|
||||
}
|
||||
}
|
||||
|
||||
if err := cfg.CheckWriteable(hostname, "oauth_token"); err != nil {
|
||||
var roErr *config.ReadOnlyEnvError
|
||||
if errors.As(err, &roErr) {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "The value of the %s environment variable is being used for authentication.\n", roErr.Variable)
|
||||
fmt.Fprint(opts.IO.ErrOut, "To have GitHub CLI store credentials instead, first clear the value from the environment.\n")
|
||||
return cmdutil.SilentError
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.Token != "" {
|
||||
// I chose to not error on existing host here; my thinking is that for --with-token the user
|
||||
// probably doesn't care if a token is overwritten since they have a token in hand they
|
||||
// explicitly want to use.
|
||||
if opts.Hostname == "" {
|
||||
return errors.New("empty hostname would leak oauth_token")
|
||||
}
|
||||
|
||||
err := cfg.Set(opts.Hostname, "oauth_token", opts.Token)
|
||||
err := cfg.Set(hostname, "oauth_token", opts.Token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = shared.ValidateHostCfg(opts.Hostname, cfg)
|
||||
apiClient, err := shared.ClientFromCfg(hostname, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := apiClient.HasMinimumScopes(hostname); err != nil {
|
||||
return fmt.Errorf("error validating token: %w", err)
|
||||
}
|
||||
|
||||
return cfg.Write()
|
||||
}
|
||||
|
||||
// TODO consider explicitly telling survey what io to use since it's implicit right now
|
||||
|
||||
hostname := opts.Hostname
|
||||
|
||||
if hostname == "" {
|
||||
var hostType int
|
||||
err := prompt.SurveyAskOne(&survey.Select{
|
||||
Message: "What account do you want to log into?",
|
||||
Options: []string{
|
||||
"GitHub.com",
|
||||
"GitHub Enterprise Server",
|
||||
},
|
||||
}, &hostType)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
|
||||
isEnterprise := hostType == 1
|
||||
|
||||
hostname = ghinstance.Default()
|
||||
if isEnterprise {
|
||||
err := prompt.SurveyAskOne(&survey.Input{
|
||||
Message: "GHE hostname:",
|
||||
}, &hostname, survey.WithValidator(ghinstance.HostnameValidator))
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(opts.IO.ErrOut, "- Logging into %s\n", hostname)
|
||||
|
||||
existingToken, _ := cfg.Get(hostname, "oauth_token")
|
||||
|
||||
if existingToken != "" && opts.Interactive {
|
||||
err := shared.ValidateHostCfg(hostname, cfg)
|
||||
if err == nil {
|
||||
apiClient, err := shared.ClientFromCfg(hostname, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient, err := shared.ClientFromCfg(hostname, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := apiClient.HasMinimumScopes(hostname); err == nil {
|
||||
username, err := api.CurrentLoginName(apiClient, hostname)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error using api: %w", err)
|
||||
|
|
@ -206,10 +192,6 @@ func loginRun(opts *LoginOptions) error {
|
|||
}
|
||||
}
|
||||
|
||||
if err := cfg.CheckWriteable(hostname, "oauth_token"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var authMode int
|
||||
if opts.Web {
|
||||
authMode = 0
|
||||
|
|
@ -244,19 +226,19 @@ func loginRun(opts *LoginOptions) error {
|
|||
return fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
|
||||
if hostname == "" {
|
||||
return errors.New("empty hostname would leak oauth_token")
|
||||
}
|
||||
|
||||
err = cfg.Set(hostname, "oauth_token", token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = shared.ValidateHostCfg(hostname, cfg)
|
||||
apiClient, err := shared.ClientFromCfg(hostname, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := apiClient.HasMinimumScopes(hostname); err != nil {
|
||||
return fmt.Errorf("error validating token: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
cs := opts.IO.ColorScheme()
|
||||
|
|
@ -322,6 +304,35 @@ func loginRun(opts *LoginOptions) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func promptForHostname() (string, error) {
|
||||
var hostType int
|
||||
err := prompt.SurveyAskOne(&survey.Select{
|
||||
Message: "What account do you want to log into?",
|
||||
Options: []string{
|
||||
"GitHub.com",
|
||||
"GitHub Enterprise Server",
|
||||
},
|
||||
}, &hostType)
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
|
||||
isEnterprise := hostType == 1
|
||||
|
||||
hostname := ghinstance.Default()
|
||||
if isEnterprise {
|
||||
err := prompt.SurveyAskOne(&survey.Input{
|
||||
Message: "GHE hostname:",
|
||||
}, &hostname, survey.WithValidator(ghinstance.HostnameValidator))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return hostname, nil
|
||||
}
|
||||
|
||||
func getAccessTokenTip(hostname string) string {
|
||||
ghHostname := hostname
|
||||
if ghHostname == "" {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ package login
|
|||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/pkg/cmd/auth/shared"
|
||||
|
|
@ -189,11 +191,13 @@ func Test_NewCmdLogin(t *testing.T) {
|
|||
|
||||
func Test_loginRun_nontty(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts *LoginOptions
|
||||
httpStubs func(*httpmock.Registry)
|
||||
wantHosts string
|
||||
wantErr *regexp.Regexp
|
||||
name string
|
||||
opts *LoginOptions
|
||||
httpStubs func(*httpmock.Registry)
|
||||
env map[string]string
|
||||
wantHosts string
|
||||
wantErr string
|
||||
wantStderr string
|
||||
}{
|
||||
{
|
||||
name: "with token",
|
||||
|
|
@ -201,6 +205,9 @@ func Test_loginRun_nontty(t *testing.T) {
|
|||
Hostname: "github.com",
|
||||
Token: "abc123",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org"))
|
||||
},
|
||||
wantHosts: "github.com:\n oauth_token: abc123\n",
|
||||
},
|
||||
{
|
||||
|
|
@ -223,7 +230,7 @@ func Test_loginRun_nontty(t *testing.T) {
|
|||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("read:org"))
|
||||
},
|
||||
wantErr: regexp.MustCompile(`missing required scope 'repo'`),
|
||||
wantErr: `error validating token: missing required scope 'repo'`,
|
||||
},
|
||||
{
|
||||
name: "missing read scope",
|
||||
|
|
@ -234,7 +241,7 @@ func Test_loginRun_nontty(t *testing.T) {
|
|||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo"))
|
||||
},
|
||||
wantErr: regexp.MustCompile(`missing required scope 'read:org'`),
|
||||
wantErr: `error validating token: missing required scope 'read:org'`,
|
||||
},
|
||||
{
|
||||
name: "has admin scope",
|
||||
|
|
@ -247,6 +254,36 @@ func Test_loginRun_nontty(t *testing.T) {
|
|||
},
|
||||
wantHosts: "github.com:\n oauth_token: abc456\n",
|
||||
},
|
||||
{
|
||||
name: "github.com token from environment",
|
||||
opts: &LoginOptions{
|
||||
Hostname: "github.com",
|
||||
Token: "abc456",
|
||||
},
|
||||
env: map[string]string{
|
||||
"GH_TOKEN": "value_from_env",
|
||||
},
|
||||
wantErr: "SilentError",
|
||||
wantStderr: heredoc.Doc(`
|
||||
The value of the GH_TOKEN environment variable is being used for authentication.
|
||||
To have GitHub CLI store credentials instead, first clear the value from the environment.
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "GHE token from environment",
|
||||
opts: &LoginOptions{
|
||||
Hostname: "ghe.io",
|
||||
Token: "abc456",
|
||||
},
|
||||
env: map[string]string{
|
||||
"GH_ENTERPRISE_TOKEN": "value_from_env",
|
||||
},
|
||||
wantErr: "SilentError",
|
||||
wantStderr: heredoc.Doc(`
|
||||
The value of the GH_ENTERPRISE_TOKEN environment variable is being used for authentication.
|
||||
To have GitHub CLI store credentials instead, first clear the value from the environment.
|
||||
`),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -256,7 +293,8 @@ func Test_loginRun_nontty(t *testing.T) {
|
|||
io.SetStdoutTTY(false)
|
||||
|
||||
tt.opts.Config = func() (config.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
cfg := config.NewBlankConfig()
|
||||
return config.InheritEnv(cfg), nil
|
||||
}
|
||||
|
||||
tt.opts.IO = io
|
||||
|
|
@ -271,10 +309,23 @@ func Test_loginRun_nontty(t *testing.T) {
|
|||
return api.NewClientFromHTTP(httpClient), nil
|
||||
}
|
||||
|
||||
old_GH_TOKEN := os.Getenv("GH_TOKEN")
|
||||
os.Setenv("GH_TOKEN", tt.env["GH_TOKEN"])
|
||||
old_GITHUB_TOKEN := os.Getenv("GITHUB_TOKEN")
|
||||
os.Setenv("GITHUB_TOKEN", tt.env["GITHUB_TOKEN"])
|
||||
old_GH_ENTERPRISE_TOKEN := os.Getenv("GH_ENTERPRISE_TOKEN")
|
||||
os.Setenv("GH_ENTERPRISE_TOKEN", tt.env["GH_ENTERPRISE_TOKEN"])
|
||||
old_GITHUB_ENTERPRISE_TOKEN := os.Getenv("GITHUB_ENTERPRISE_TOKEN")
|
||||
os.Setenv("GITHUB_ENTERPRISE_TOKEN", tt.env["GITHUB_ENTERPRISE_TOKEN"])
|
||||
defer func() {
|
||||
os.Setenv("GH_TOKEN", old_GH_TOKEN)
|
||||
os.Setenv("GITHUB_TOKEN", old_GITHUB_TOKEN)
|
||||
os.Setenv("GH_ENTERPRISE_TOKEN", old_GH_ENTERPRISE_TOKEN)
|
||||
os.Setenv("GITHUB_ENTERPRISE_TOKEN", old_GITHUB_ENTERPRISE_TOKEN)
|
||||
}()
|
||||
|
||||
if tt.httpStubs != nil {
|
||||
tt.httpStubs(reg)
|
||||
} else {
|
||||
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org"))
|
||||
}
|
||||
|
||||
mainBuf := bytes.Buffer{}
|
||||
|
|
@ -282,18 +333,14 @@ func Test_loginRun_nontty(t *testing.T) {
|
|||
defer config.StubWriteConfig(&mainBuf, &hostsBuf)()
|
||||
|
||||
err := loginRun(tt.opts)
|
||||
assert.Equal(t, tt.wantErr == nil, err == nil)
|
||||
if err != nil {
|
||||
if tt.wantErr != nil {
|
||||
assert.True(t, tt.wantErr.MatchString(err.Error()))
|
||||
return
|
||||
} else {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
if tt.wantErr != "" {
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", stdout.String())
|
||||
assert.Equal(t, "", stderr.String())
|
||||
assert.Equal(t, tt.wantStderr, stderr.String())
|
||||
assert.Equal(t, tt.wantHosts, hostsBuf.String())
|
||||
reg.Verify(t)
|
||||
})
|
||||
|
|
@ -319,7 +366,7 @@ func Test_loginRun_Survey(t *testing.T) {
|
|||
_ = cfg.Set("github.com", "oauth_token", "ghi789")
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,"))
|
||||
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org"))
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`))
|
||||
|
|
@ -329,7 +376,7 @@ func Test_loginRun_Survey(t *testing.T) {
|
|||
as.StubOne(false) // do not continue
|
||||
},
|
||||
wantHosts: "", // nothing should have been written to hosts
|
||||
wantErrOut: regexp.MustCompile("Logging into github.com"),
|
||||
wantErrOut: nil,
|
||||
},
|
||||
{
|
||||
name: "hostname set",
|
||||
|
|
@ -345,7 +392,7 @@ func Test_loginRun_Survey(t *testing.T) {
|
|||
as.StubOne(false) // cache credentials
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,"))
|
||||
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org"))
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`))
|
||||
|
|
@ -367,7 +414,7 @@ func Test_loginRun_Survey(t *testing.T) {
|
|||
as.StubOne(false) // cache credentials
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,"))
|
||||
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,read:public_key"))
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`))
|
||||
|
|
@ -440,7 +487,7 @@ func Test_loginRun_Survey(t *testing.T) {
|
|||
if tt.httpStubs != nil {
|
||||
tt.httpStubs(reg)
|
||||
} else {
|
||||
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,"))
|
||||
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,read:public_key"))
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`))
|
||||
|
|
|
|||
|
|
@ -105,6 +105,12 @@ func logoutRun(opts *LogoutOptions) error {
|
|||
}
|
||||
|
||||
if err := cfg.CheckWriteable(hostname, "oauth_token"); err != nil {
|
||||
var roErr *config.ReadOnlyEnvError
|
||||
if errors.As(err, &roErr) {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "The value of the %s environment variable is being used for authentication.\n", roErr.Variable)
|
||||
fmt.Fprint(opts.IO.ErrOut, "To erase credentials stored in GitHub CLI, first clear the value from the environment.\n")
|
||||
return cmdutil.SilentError
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ func Test_logoutRun_tty(t *testing.T) {
|
|||
cfgHosts []string
|
||||
wantHosts string
|
||||
wantErrOut *regexp.Regexp
|
||||
wantErr *regexp.Regexp
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "no arguments, multiple hosts",
|
||||
|
|
@ -123,7 +123,7 @@ func Test_logoutRun_tty(t *testing.T) {
|
|||
{
|
||||
name: "no arguments, no hosts",
|
||||
opts: &LogoutOptions{},
|
||||
wantErr: regexp.MustCompile(`not logged in to any hosts`),
|
||||
wantErr: `not logged in to any hosts`,
|
||||
},
|
||||
{
|
||||
name: "hostname",
|
||||
|
|
@ -176,14 +176,11 @@ func Test_logoutRun_tty(t *testing.T) {
|
|||
}
|
||||
|
||||
err := logoutRun(tt.opts)
|
||||
assert.Equal(t, tt.wantErr == nil, err == nil)
|
||||
if err != nil {
|
||||
if tt.wantErr != nil {
|
||||
assert.True(t, tt.wantErr.MatchString(err.Error()))
|
||||
return
|
||||
} else {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
if tt.wantErr != "" {
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
return
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
if tt.wantErrOut == nil {
|
||||
|
|
@ -204,7 +201,7 @@ func Test_logoutRun_nontty(t *testing.T) {
|
|||
opts *LogoutOptions
|
||||
cfgHosts []string
|
||||
wantHosts string
|
||||
wantErr *regexp.Regexp
|
||||
wantErr string
|
||||
ghtoken string
|
||||
}{
|
||||
{
|
||||
|
|
@ -227,7 +224,7 @@ func Test_logoutRun_nontty(t *testing.T) {
|
|||
opts: &LogoutOptions{
|
||||
Hostname: "harry.mason",
|
||||
},
|
||||
wantErr: regexp.MustCompile(`not logged in to any hosts`),
|
||||
wantErr: `not logged in to any hosts`,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -258,16 +255,10 @@ func Test_logoutRun_nontty(t *testing.T) {
|
|||
defer config.StubWriteConfig(&mainBuf, &hostsBuf)()
|
||||
|
||||
err := logoutRun(tt.opts)
|
||||
assert.Equal(t, tt.wantErr == nil, err == nil)
|
||||
if err != nil {
|
||||
if tt.wantErr != nil {
|
||||
if !tt.wantErr.MatchString(err.Error()) {
|
||||
t.Errorf("got error: %v", err)
|
||||
}
|
||||
return
|
||||
} else {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
if tt.wantErr != "" {
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", stderr.String())
|
||||
|
|
|
|||
|
|
@ -112,6 +112,12 @@ func refreshRun(opts *RefreshOptions) error {
|
|||
}
|
||||
|
||||
if err := cfg.CheckWriteable(hostname, "oauth_token"); err != nil {
|
||||
var roErr *config.ReadOnlyEnvError
|
||||
if errors.As(err, &roErr) {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "The value of the %s environment variable is being used for authentication.\n", roErr.Variable)
|
||||
fmt.Fprint(opts.IO.ErrOut, "To refresh credentials stored in GitHub CLI, first clear the value from the environment.\n")
|
||||
return cmdutil.SilentError
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package refresh
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/internal/config"
|
||||
|
|
@ -134,14 +133,14 @@ func Test_refreshRun(t *testing.T) {
|
|||
opts *RefreshOptions
|
||||
askStubs func(*prompt.AskStubber)
|
||||
cfgHosts []string
|
||||
wantErr *regexp.Regexp
|
||||
wantErr string
|
||||
nontty bool
|
||||
wantAuthArgs authArgs
|
||||
}{
|
||||
{
|
||||
name: "no hosts configured",
|
||||
opts: &RefreshOptions{},
|
||||
wantErr: regexp.MustCompile(`not logged in to any hosts`),
|
||||
wantErr: `not logged in to any hosts`,
|
||||
},
|
||||
{
|
||||
name: "hostname given but dne",
|
||||
|
|
@ -152,7 +151,7 @@ func Test_refreshRun(t *testing.T) {
|
|||
opts: &RefreshOptions{
|
||||
Hostname: "obed.morton",
|
||||
},
|
||||
wantErr: regexp.MustCompile(`not logged in to obed.morton`),
|
||||
wantErr: `not logged in to obed.morton`,
|
||||
},
|
||||
{
|
||||
name: "hostname provided and is configured",
|
||||
|
|
@ -250,14 +249,12 @@ func Test_refreshRun(t *testing.T) {
|
|||
}
|
||||
|
||||
err := refreshRun(tt.opts)
|
||||
assert.Equal(t, tt.wantErr == nil, err == nil)
|
||||
if err != nil {
|
||||
if tt.wantErr != nil {
|
||||
assert.True(t, tt.wantErr.MatchString(err.Error()))
|
||||
return
|
||||
} else {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
if tt.wantErr != "" {
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), tt.wantErr)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
assert.Equal(t, aa.hostname, tt.wantAuthArgs.hostname)
|
||||
|
|
|
|||
|
|
@ -8,20 +8,6 @@ import (
|
|||
"github.com/cli/cli/internal/config"
|
||||
)
|
||||
|
||||
func ValidateHostCfg(hostname string, cfg config.Config) error {
|
||||
apiClient, err := ClientFromCfg(hostname, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = apiClient.HasMinimumScopes(hostname)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not validate token: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var ClientFromCfg = func(hostname string, cfg config.Config) (*api.Client, error) {
|
||||
var opts []api.ClientOption
|
||||
|
||||
|
|
|
|||
|
|
@ -81,11 +81,13 @@ func statusRun(opts *StatusOptions) error {
|
|||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
var failed bool
|
||||
var isHostnameFound bool
|
||||
|
||||
for _, hostname := range hostnames {
|
||||
if opts.Hostname != "" && opts.Hostname != hostname {
|
||||
continue
|
||||
}
|
||||
isHostnameFound = true
|
||||
|
||||
token, tokenSource, _ := cfg.GetWithSource(hostname, "oauth_token")
|
||||
tokenIsWriteable := cfg.CheckWriteable(hostname, "oauth_token") == nil
|
||||
|
|
@ -139,6 +141,12 @@ func statusRun(opts *StatusOptions) error {
|
|||
// not to since I wanted this command to be read-only.
|
||||
}
|
||||
|
||||
if !isHostnameFound {
|
||||
fmt.Fprintf(stderr,
|
||||
"Hostname %q not found among authenticated GitHub hosts\n", opts.Hostname)
|
||||
return cmdutil.SilentError
|
||||
}
|
||||
|
||||
for _, hostname := range hostnames {
|
||||
lines, ok := statusInfo[hostname]
|
||||
if !ok {
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ func Test_statusRun(t *testing.T) {
|
|||
opts *StatusOptions
|
||||
httpStubs func(*httpmock.Registry)
|
||||
cfg func(config.Config)
|
||||
wantErr *regexp.Regexp
|
||||
wantErr string
|
||||
wantErrOut *regexp.Regexp
|
||||
}{
|
||||
{
|
||||
|
|
@ -91,7 +91,7 @@ func Test_statusRun(t *testing.T) {
|
|||
_ = c.Set("github.com", "oauth_token", "abc123")
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,"))
|
||||
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org"))
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`))
|
||||
|
|
@ -106,14 +106,14 @@ func Test_statusRun(t *testing.T) {
|
|||
_ = c.Set("github.com", "oauth_token", "abc123")
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,"))
|
||||
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,"))
|
||||
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo"))
|
||||
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org"))
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`))
|
||||
},
|
||||
wantErrOut: regexp.MustCompile(`joel.miller: missing required.*Logged in to github.com as.*tess`),
|
||||
wantErr: regexp.MustCompile(``),
|
||||
wantErr: "SilentError",
|
||||
},
|
||||
{
|
||||
name: "bad token",
|
||||
|
|
@ -124,13 +124,13 @@ func Test_statusRun(t *testing.T) {
|
|||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.StatusStringResponse(400, "no bueno"))
|
||||
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,"))
|
||||
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org"))
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`))
|
||||
},
|
||||
wantErrOut: regexp.MustCompile(`joel.miller: authentication failed.*Logged in to github.com as.*tess`),
|
||||
wantErr: regexp.MustCompile(``),
|
||||
wantErr: "SilentError",
|
||||
},
|
||||
{
|
||||
name: "all good",
|
||||
|
|
@ -140,8 +140,8 @@ func Test_statusRun(t *testing.T) {
|
|||
_ = c.Set("github.com", "oauth_token", "abc123")
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,"))
|
||||
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,"))
|
||||
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org"))
|
||||
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org"))
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`))
|
||||
|
|
@ -159,8 +159,8 @@ func Test_statusRun(t *testing.T) {
|
|||
_ = c.Set("github.com", "oauth_token", "xyz456")
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,"))
|
||||
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,"))
|
||||
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org"))
|
||||
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org"))
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`))
|
||||
|
|
@ -180,8 +180,8 @@ func Test_statusRun(t *testing.T) {
|
|||
_ = c.Set("github.com", "oauth_token", "xyz456")
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,"))
|
||||
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,"))
|
||||
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org"))
|
||||
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org"))
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`))
|
||||
|
|
@ -190,6 +190,17 @@ func Test_statusRun(t *testing.T) {
|
|||
httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`))
|
||||
},
|
||||
wantErrOut: regexp.MustCompile(`(?s)Token: xyz456.*Token: abc123`),
|
||||
}, {
|
||||
name: "missing hostname",
|
||||
opts: &StatusOptions{
|
||||
Hostname: "github.example.com",
|
||||
},
|
||||
cfg: func(c config.Config) {
|
||||
_ = c.Set("github.com", "oauth_token", "abc123")
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {},
|
||||
wantErrOut: regexp.MustCompile(`(?s)Hostname "github.example.com" not found among authenticated GitHub hosts`),
|
||||
wantErr: "SilentError",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -236,14 +247,11 @@ func Test_statusRun(t *testing.T) {
|
|||
defer config.StubWriteConfig(&mainBuf, &hostsBuf)()
|
||||
|
||||
err := statusRun(tt.opts)
|
||||
assert.Equal(t, tt.wantErr == nil, err == nil)
|
||||
if err != nil {
|
||||
if tt.wantErr != nil {
|
||||
assert.True(t, tt.wantErr.MatchString(err.Error()))
|
||||
return
|
||||
} else {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
if tt.wantErr != "" {
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
return
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
if tt.wantErrOut == nil {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/config"
|
||||
|
|
@ -12,6 +13,46 @@ import (
|
|||
"github.com/cli/cli/pkg/iostreams"
|
||||
)
|
||||
|
||||
var timezoneNames = map[int]string{
|
||||
-39600: "Pacific/Niue",
|
||||
-36000: "Pacific/Honolulu",
|
||||
-34200: "Pacific/Marquesas",
|
||||
-32400: "America/Anchorage",
|
||||
-28800: "America/Los_Angeles",
|
||||
-25200: "America/Chihuahua",
|
||||
-21600: "America/Chicago",
|
||||
-18000: "America/Bogota",
|
||||
-14400: "America/Caracas",
|
||||
-12600: "America/St_Johns",
|
||||
-10800: "America/Argentina/Buenos_Aires",
|
||||
-7200: "Atlantic/South_Georgia",
|
||||
-3600: "Atlantic/Cape_Verde",
|
||||
0: "Europe/London",
|
||||
3600: "Europe/Amsterdam",
|
||||
7200: "Europe/Athens",
|
||||
10800: "Europe/Istanbul",
|
||||
12600: "Asia/Tehran",
|
||||
14400: "Asia/Dubai",
|
||||
16200: "Asia/Kabul",
|
||||
18000: "Asia/Tashkent",
|
||||
19800: "Asia/Kolkata",
|
||||
20700: "Asia/Kathmandu",
|
||||
21600: "Asia/Dhaka",
|
||||
23400: "Asia/Rangoon",
|
||||
25200: "Asia/Bangkok",
|
||||
28800: "Asia/Manila",
|
||||
31500: "Australia/Eucla",
|
||||
32400: "Asia/Tokyo",
|
||||
34200: "Australia/Darwin",
|
||||
36000: "Australia/Brisbane",
|
||||
37800: "Australia/Adelaide",
|
||||
39600: "Pacific/Guadalcanal",
|
||||
43200: "Pacific/Nauru",
|
||||
46800: "Pacific/Auckland",
|
||||
49500: "Pacific/Chatham",
|
||||
50400: "Pacific/Kiritimati",
|
||||
}
|
||||
|
||||
// generic authenticated HTTP client for commands
|
||||
func NewHTTPClient(io *iostreams.IOStreams, cfg config.Config, appVersion string, setAccept bool) *http.Client {
|
||||
var opts []api.ClientOption
|
||||
|
|
@ -29,6 +70,16 @@ func NewHTTPClient(io *iostreams.IOStreams, cfg config.Config, appVersion string
|
|||
}
|
||||
return "", nil
|
||||
}),
|
||||
api.AddHeaderFunc("Time-Zone", func(req *http.Request) (string, error) {
|
||||
if req.Method != "GET" && req.Method != "HEAD" {
|
||||
if time.Local.String() != "Local" {
|
||||
return time.Local.String(), nil
|
||||
}
|
||||
_, offset := time.Now().Zone()
|
||||
return timezoneNames[offset], nil
|
||||
}
|
||||
return "", nil
|
||||
}),
|
||||
)
|
||||
|
||||
if setAccept {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/run"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
|
|
@ -88,13 +89,15 @@ func Test_GistClone(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
cs, restore := test.InitCmdStubber()
|
||||
defer restore()
|
||||
|
||||
cs.Stub("") // git clone
|
||||
cs, restore := run.Stub()
|
||||
defer restore(t)
|
||||
cs.Register(`git clone`, 0, "", func(s []string) {
|
||||
assert.Equal(t, tt.want, strings.Join(s, " "))
|
||||
})
|
||||
|
||||
output, err := runCloneCommand(httpClient, tt.args)
|
||||
if err != nil {
|
||||
|
|
@ -103,9 +106,6 @@ func Test_GistClone(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
assert.Equal(t, 1, cs.Count)
|
||||
assert.Equal(t, tt.want, strings.Join(cs.Calls[0].Args, " "))
|
||||
reg.Verify(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ package create
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"github.com/cli/cli/test"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/internal/run"
|
||||
"github.com/cli/cli/pkg/cmd/gist/shared"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
|
|
@ -291,11 +291,10 @@ func Test_createRun(t *testing.T) {
|
|||
io, stdin, stdout, stderr := iostreams.Test()
|
||||
tt.opts.IO = io
|
||||
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
|
||||
cs, teardown := run.Stub()
|
||||
defer teardown(t)
|
||||
if tt.opts.WebMode {
|
||||
cs.Stub("")
|
||||
cs.Register(`https://gist\.github\.com/aa5a315d61ae9438b18d$`, 0, "")
|
||||
}
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
@ -313,12 +312,6 @@ func Test_createRun(t *testing.T) {
|
|||
assert.Equal(t, tt.wantOut, stdout.String())
|
||||
assert.Equal(t, tt.wantStderr, stderr.String())
|
||||
assert.Equal(t, tt.wantParams, reqBody)
|
||||
|
||||
if tt.opts.WebMode {
|
||||
browserCall := cs.Calls[0].Args
|
||||
assert.Equal(t, browserCall[len(browserCall)-1], "https://gist.github.com/aa5a315d61ae9438b18d")
|
||||
}
|
||||
|
||||
reg.Verify(t)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@ package delete
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/ghinstance"
|
||||
"github.com/cli/cli/pkg/cmd/gist/shared"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type DeleteOptions struct {
|
||||
|
|
@ -28,7 +29,7 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co
|
|||
cmd := &cobra.Command{
|
||||
Use: "delete {<gist ID> | <gist URL>}",
|
||||
Short: "Delete a gist",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Args: cmdutil.MinimumArgs(1, "cannot delete: gist argument required"),
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
opts.Selector = args[0]
|
||||
if runF != nil {
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
|
|||
cmd := &cobra.Command{
|
||||
Use: "edit {<gist ID> | <gist URL>}",
|
||||
Short: "Edit one of your gists",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Args: cmdutil.MinimumArgs(1, "cannot edit: gist argument required"),
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
opts.Selector = args[0]
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
|
|||
cmd := &cobra.Command{
|
||||
Use: "view {<gist id> | <gist url>}",
|
||||
Short: "View a gist",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Args: cmdutil.MinimumArgs(1, "cannot view: gist argument required"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.Selector = args[0]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,59 +1,28 @@
|
|||
package comment
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/cmd/issue/shared"
|
||||
issueShared "github.com/cli/cli/pkg/cmd/issue/shared"
|
||||
prShared "github.com/cli/cli/pkg/cmd/pr/shared"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/pkg/surveyext"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type CommentOptions struct {
|
||||
HttpClient func() (*http.Client, error)
|
||||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
EditSurvey func() (string, error)
|
||||
InputTypeSurvey func() (inputType, error)
|
||||
ConfirmSubmitSurvey func() (bool, error)
|
||||
OpenInBrowser func(string) error
|
||||
|
||||
SelectorArg string
|
||||
Interactive bool
|
||||
InputType inputType
|
||||
Body string
|
||||
}
|
||||
|
||||
type inputType int
|
||||
|
||||
const (
|
||||
inputTypeEditor inputType = iota
|
||||
inputTypeInline
|
||||
inputTypeWeb
|
||||
)
|
||||
|
||||
func NewCmdComment(f *cmdutil.Factory, runF func(*CommentOptions) error) *cobra.Command {
|
||||
opts := &CommentOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
EditSurvey: editSurvey(f.Config, f.IOStreams),
|
||||
InputTypeSurvey: inputTypeSurvey,
|
||||
ConfirmSubmitSurvey: confirmSubmitSurvey,
|
||||
OpenInBrowser: utils.OpenInBrowser,
|
||||
func NewCmdComment(f *cmdutil.Factory, runF func(*prShared.CommentableOptions) error) *cobra.Command {
|
||||
opts := &prShared.CommentableOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
EditSurvey: prShared.CommentableEditSurvey(f.Config, f.IOStreams),
|
||||
InteractiveEditSurvey: prShared.CommentableInteractiveEditSurvey(f.Config, f.IOStreams),
|
||||
ConfirmSubmitSurvey: prShared.CommentableConfirmSubmitSurvey,
|
||||
OpenInBrowser: utils.OpenInBrowser,
|
||||
}
|
||||
|
||||
var webMode bool
|
||||
var editorMode bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "comment {<number> | <url>}",
|
||||
Short: "Create a new issue comment",
|
||||
|
|
@ -61,140 +30,40 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*CommentOptions) error) *cobra.
|
|||
$ gh issue comment 22 --body "I was able to reproduce this issue, lets fix it."
|
||||
`),
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
opts.SelectorArg = args[0]
|
||||
|
||||
inputFlags := 0
|
||||
if cmd.Flags().Changed("body") {
|
||||
opts.InputType = inputTypeInline
|
||||
inputFlags++
|
||||
}
|
||||
if webMode {
|
||||
opts.InputType = inputTypeWeb
|
||||
inputFlags++
|
||||
}
|
||||
if editorMode {
|
||||
opts.InputType = inputTypeEditor
|
||||
inputFlags++
|
||||
}
|
||||
|
||||
if inputFlags == 0 {
|
||||
if !opts.IO.CanPrompt() {
|
||||
return &cmdutil.FlagError{Err: errors.New("--body or --web required when not running interactively")}
|
||||
}
|
||||
opts.Interactive = true
|
||||
} else if inputFlags == 1 {
|
||||
if !opts.IO.CanPrompt() && opts.InputType == inputTypeEditor {
|
||||
return &cmdutil.FlagError{Err: errors.New("--body or --web required when not running interactively")}
|
||||
}
|
||||
} else if inputFlags > 1 {
|
||||
return &cmdutil.FlagError{Err: fmt.Errorf("specify only one of --body, --editor, or --web")}
|
||||
}
|
||||
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.RetrieveCommentable = retrieveIssue(f.HttpClient, f.BaseRepo, args[0])
|
||||
return prShared.CommentablePreRun(cmd, opts)
|
||||
},
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
return commentRun(opts)
|
||||
return prShared.CommentableRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "Supply a body. Will prompt for one otherwise.")
|
||||
cmd.Flags().BoolVarP(&editorMode, "editor", "e", false, "Add body using editor")
|
||||
cmd.Flags().BoolVarP(&webMode, "web", "w", false, "Add body in browser")
|
||||
cmd.Flags().BoolP("editor", "e", false, "Add body using editor")
|
||||
cmd.Flags().BoolP("web", "w", false, "Add body in browser")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func commentRun(opts *CommentOptions) error {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
issue, baseRepo, err := shared.IssueFromArg(apiClient, opts.BaseRepo, opts.SelectorArg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.Interactive {
|
||||
inputType, err := opts.InputTypeSurvey()
|
||||
func retrieveIssue(httpClient func() (*http.Client, error),
|
||||
baseRepo func() (ghrepo.Interface, error),
|
||||
selector string) func() (prShared.Commentable, ghrepo.Interface, error) {
|
||||
return func() (prShared.Commentable, ghrepo.Interface, error) {
|
||||
httpClient, err := httpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
opts.InputType = inputType
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
switch opts.InputType {
|
||||
case inputTypeWeb:
|
||||
openURL := issue.URL + "#issuecomment-new"
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
|
||||
}
|
||||
return opts.OpenInBrowser(openURL)
|
||||
case inputTypeEditor:
|
||||
body, err := opts.EditSurvey()
|
||||
issue, repo, err := issueShared.IssueFromArg(apiClient, baseRepo, selector)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
opts.Body = body
|
||||
}
|
||||
|
||||
if opts.Interactive {
|
||||
cont, err := opts.ConfirmSubmitSurvey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !cont {
|
||||
return fmt.Errorf("Discarding...")
|
||||
}
|
||||
}
|
||||
|
||||
params := api.CommentCreateInput{Body: opts.Body, SubjectId: issue.ID}
|
||||
url, err := api.CommentCreate(apiClient, baseRepo.RepoHost(), params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(opts.IO.Out, url)
|
||||
return nil
|
||||
}
|
||||
|
||||
var inputTypeSurvey = func() (inputType, error) {
|
||||
var result int
|
||||
inputTypeQuestion := &survey.Select{
|
||||
Message: "Where do you want to draft your comment?",
|
||||
Options: []string{"Editor", "Web"},
|
||||
}
|
||||
err := survey.AskOne(inputTypeQuestion, &result)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if result == 0 {
|
||||
return inputTypeEditor, nil
|
||||
} else {
|
||||
return inputTypeWeb, nil
|
||||
}
|
||||
}
|
||||
|
||||
var confirmSubmitSurvey = func() (bool, error) {
|
||||
var confirm bool
|
||||
submit := &survey.Confirm{
|
||||
Message: "Submit?",
|
||||
Default: true,
|
||||
}
|
||||
err := survey.AskOne(submit, &confirm)
|
||||
return confirm, err
|
||||
}
|
||||
|
||||
var editSurvey = func(cf func() (config.Config, error), io *iostreams.IOStreams) func() (string, error) {
|
||||
return func() (string, error) {
|
||||
editorCommand, err := cmdutil.DetermineEditor(cf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return surveyext.Edit(editorCommand, "*.md", "", io.In, io.Out, io.ErrOut, nil)
|
||||
return issue, repo, nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/cmd/pr/shared"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
|
|
@ -17,20 +18,19 @@ func TestNewCmdComment(t *testing.T) {
|
|||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
output CommentOptions
|
||||
output shared.CommentableOptions
|
||||
wantsErr bool
|
||||
}{
|
||||
{
|
||||
name: "no arguments",
|
||||
input: "",
|
||||
output: CommentOptions{},
|
||||
output: shared.CommentableOptions{},
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "issue number",
|
||||
input: "1",
|
||||
output: CommentOptions{
|
||||
SelectorArg: "1",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
Body: "",
|
||||
|
|
@ -40,8 +40,7 @@ func TestNewCmdComment(t *testing.T) {
|
|||
{
|
||||
name: "issue url",
|
||||
input: "https://github.com/OWNER/REPO/issues/12",
|
||||
output: CommentOptions{
|
||||
SelectorArg: "https://github.com/OWNER/REPO/issues/12",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
Body: "",
|
||||
|
|
@ -51,10 +50,9 @@ func TestNewCmdComment(t *testing.T) {
|
|||
{
|
||||
name: "body flag",
|
||||
input: "1 --body test",
|
||||
output: CommentOptions{
|
||||
SelectorArg: "1",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: inputTypeInline,
|
||||
InputType: shared.InputTypeInline,
|
||||
Body: "test",
|
||||
},
|
||||
wantsErr: false,
|
||||
|
|
@ -62,10 +60,9 @@ func TestNewCmdComment(t *testing.T) {
|
|||
{
|
||||
name: "editor flag",
|
||||
input: "1 --editor",
|
||||
output: CommentOptions{
|
||||
SelectorArg: "1",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: inputTypeEditor,
|
||||
InputType: shared.InputTypeEditor,
|
||||
Body: "",
|
||||
},
|
||||
wantsErr: false,
|
||||
|
|
@ -73,10 +70,9 @@ func TestNewCmdComment(t *testing.T) {
|
|||
{
|
||||
name: "web flag",
|
||||
input: "1 --web",
|
||||
output: CommentOptions{
|
||||
SelectorArg: "1",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: inputTypeWeb,
|
||||
InputType: shared.InputTypeWeb,
|
||||
Body: "",
|
||||
},
|
||||
wantsErr: false,
|
||||
|
|
@ -84,25 +80,25 @@ func TestNewCmdComment(t *testing.T) {
|
|||
{
|
||||
name: "editor and web flags",
|
||||
input: "1 --editor --web",
|
||||
output: CommentOptions{},
|
||||
output: shared.CommentableOptions{},
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "editor and body flags",
|
||||
input: "1 --editor --body test",
|
||||
output: CommentOptions{},
|
||||
output: shared.CommentableOptions{},
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "web and body flags",
|
||||
input: "1 --web --body test",
|
||||
output: CommentOptions{},
|
||||
output: shared.CommentableOptions{},
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "editor, web, and body flags",
|
||||
input: "1 --editor --web --body test",
|
||||
output: CommentOptions{},
|
||||
output: shared.CommentableOptions{},
|
||||
wantsErr: true,
|
||||
},
|
||||
}
|
||||
|
|
@ -121,8 +117,8 @@ func TestNewCmdComment(t *testing.T) {
|
|||
argv, err := shlex.Split(tt.input)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts *CommentOptions
|
||||
cmd := NewCmdComment(f, func(opts *CommentOptions) error {
|
||||
var gotOpts *shared.CommentableOptions
|
||||
cmd := NewCmdComment(f, func(opts *shared.CommentableOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
|
|
@ -140,7 +136,6 @@ func TestNewCmdComment(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.output.SelectorArg, gotOpts.SelectorArg)
|
||||
assert.Equal(t, tt.output.Interactive, gotOpts.Interactive)
|
||||
assert.Equal(t, tt.output.InputType, gotOpts.InputType)
|
||||
assert.Equal(t, tt.output.Body, gotOpts.Body)
|
||||
|
|
@ -151,38 +146,20 @@ func TestNewCmdComment(t *testing.T) {
|
|||
func Test_commentRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input *CommentOptions
|
||||
input *shared.CommentableOptions
|
||||
httpStubs func(*testing.T, *httpmock.Registry)
|
||||
stdout string
|
||||
stderr string
|
||||
}{
|
||||
{
|
||||
name: "interactive web",
|
||||
input: &CommentOptions{
|
||||
SelectorArg: "123",
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
Body: "",
|
||||
|
||||
InputTypeSurvey: func() (inputType, error) { return inputTypeWeb, nil },
|
||||
OpenInBrowser: func(string) error { return nil },
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockIssueFromNumber(t, reg)
|
||||
},
|
||||
stderr: "Opening github.com/OWNER/REPO/issues/123 in your browser.\n",
|
||||
},
|
||||
{
|
||||
name: "interactive editor",
|
||||
input: &CommentOptions{
|
||||
SelectorArg: "123",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
Body: "",
|
||||
|
||||
EditSurvey: func() (string, error) { return "comment body", nil },
|
||||
InputTypeSurvey: func() (inputType, error) { return inputTypeEditor, nil },
|
||||
ConfirmSubmitSurvey: func() (bool, error) { return true, nil },
|
||||
InteractiveEditSurvey: func() (string, error) { return "comment body", nil },
|
||||
ConfirmSubmitSurvey: func() (bool, error) { return true, nil },
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockIssueFromNumber(t, reg)
|
||||
|
|
@ -192,10 +169,9 @@ func Test_commentRun(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "non-interactive web",
|
||||
input: &CommentOptions{
|
||||
SelectorArg: "123",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: inputTypeWeb,
|
||||
InputType: shared.InputTypeWeb,
|
||||
Body: "",
|
||||
|
||||
OpenInBrowser: func(string) error { return nil },
|
||||
|
|
@ -207,10 +183,9 @@ func Test_commentRun(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "non-interactive editor",
|
||||
input: &CommentOptions{
|
||||
SelectorArg: "123",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: inputTypeEditor,
|
||||
InputType: shared.InputTypeEditor,
|
||||
Body: "",
|
||||
|
||||
EditSurvey: func() (string, error) { return "comment body", nil },
|
||||
|
|
@ -223,10 +198,9 @@ func Test_commentRun(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "non-interactive inline",
|
||||
input: &CommentOptions{
|
||||
SelectorArg: "123",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: inputTypeInline,
|
||||
InputType: shared.InputTypeInline,
|
||||
Body: "comment body",
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
|
|
@ -246,16 +220,15 @@ func Test_commentRun(t *testing.T) {
|
|||
defer reg.Verify(t)
|
||||
tt.httpStubs(t, reg)
|
||||
|
||||
httpClient := func() (*http.Client, error) { return &http.Client{Transport: reg}, nil }
|
||||
baseRepo := func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil }
|
||||
|
||||
tt.input.IO = io
|
||||
tt.input.HttpClient = func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
tt.input.BaseRepo = func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
}
|
||||
tt.input.HttpClient = httpClient
|
||||
tt.input.RetrieveCommentable = retrieveIssue(tt.input.HttpClient, baseRepo, "123")
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := commentRun(tt.input)
|
||||
err := shared.CommentableRun(tt.input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.stdout, stdout.String())
|
||||
assert.Equal(t, tt.stderr, stderr.String())
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/cmd/pr/shared"
|
||||
prShared "github.com/cli/cli/pkg/cmd/pr/shared"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
|
|
@ -53,6 +54,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
$ gh issue create --label "bug,help wanted"
|
||||
$ gh issue create --label bug --label "help wanted"
|
||||
$ gh issue create --assignee monalisa,hubot
|
||||
$ gh issue create --assignee @me
|
||||
$ gh issue create --project "Roadmap"
|
||||
`),
|
||||
Args: cmdutil.NoArgsQuoteReminder,
|
||||
|
|
@ -84,7 +86,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
cmd.Flags().StringVarP(&opts.Title, "title", "t", "", "Supply a title. Will prompt for one otherwise.")
|
||||
cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "Supply a body. Will prompt for one otherwise.")
|
||||
cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "Open the browser to create an issue")
|
||||
cmd.Flags().StringSliceVarP(&opts.Assignees, "assignee", "a", nil, "Assign people by their `login`")
|
||||
cmd.Flags().StringSliceVarP(&opts.Assignees, "assignee", "a", nil, "Assign people by their `login`. Use \"@me\" to self-assign.")
|
||||
cmd.Flags().StringSliceVarP(&opts.Labels, "label", "l", nil, "Add labels by `name`")
|
||||
cmd.Flags().StringSliceVarP(&opts.Projects, "project", "p", nil, "Add the issue to projects by `name`")
|
||||
cmd.Flags().StringVarP(&opts.Milestone, "milestone", "m", "", "Add the issue to a milestone by `name`")
|
||||
|
|
@ -114,9 +116,15 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
milestones = []string{opts.Milestone}
|
||||
}
|
||||
|
||||
meReplacer := shared.NewMeReplacer(apiClient, baseRepo.RepoHost())
|
||||
assignees, err := meReplacer.ReplaceSlice(opts.Assignees)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tb := prShared.IssueMetadataState{
|
||||
Type: prShared.IssueMetadata,
|
||||
Assignees: opts.Assignees,
|
||||
Assignees: assignees,
|
||||
Labels: opts.Labels,
|
||||
Projects: opts.Projects,
|
||||
Milestones: milestones,
|
||||
|
|
@ -134,7 +142,14 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
|
||||
if opts.WebMode {
|
||||
openURL := ghrepo.GenerateRepoURL(baseRepo, "issues/new")
|
||||
if opts.Title != "" || opts.Body != "" {
|
||||
if opts.Title != "" || opts.Body != "" || tb.HasMetadata() {
|
||||
if len(opts.Projects) > 0 {
|
||||
var err error
|
||||
tb.Projects, err = api.ProjectNamesToPaths(apiClient, baseRepo, tb.Projects)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not add to project: %w", err)
|
||||
}
|
||||
}
|
||||
openURL, err = prShared.WithPrAndIssueQueryParams(openURL, tb)
|
||||
if err != nil {
|
||||
return
|
||||
|
|
@ -235,6 +250,13 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
}
|
||||
|
||||
if action == prShared.PreviewAction {
|
||||
if len(tb.Projects) > 0 {
|
||||
var err error
|
||||
tb.Projects, err = api.ProjectNamesToPaths(apiClient, repo, tb.Projects)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not add to project: %w", err)
|
||||
}
|
||||
}
|
||||
openURL := ghrepo.GenerateRepoURL(baseRepo, "issues/new")
|
||||
openURL, err = prShared.WithPrAndIssueQueryParams(openURL, tb)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
|
|
@ -26,13 +25,6 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func eq(t *testing.T, got interface{}, expected interface{}) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(got, expected) {
|
||||
t.Errorf("expected: %v, got: %v", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, error) {
|
||||
return runCommandWithRootDirOverridden(rt, isTTY, cli, "")
|
||||
}
|
||||
|
|
@ -83,11 +75,7 @@ func TestIssueCreate_nontty_error(t *testing.T) {
|
|||
defer http.Verify(t)
|
||||
|
||||
_, err := runCommand(http, false, `-t hello`)
|
||||
if err == nil {
|
||||
t.Fatal("expected error running command `issue create`")
|
||||
}
|
||||
|
||||
assert.Equal(t, "must provide --title and --body when not running interactively", err.Error())
|
||||
assert.EqualError(t, err, "must provide --title and --body when not running interactively")
|
||||
}
|
||||
|
||||
func TestIssueCreate(t *testing.T) {
|
||||
|
|
@ -120,7 +108,7 @@ func TestIssueCreate(t *testing.T) {
|
|||
t.Errorf("error running command `issue create`: %v", err)
|
||||
}
|
||||
|
||||
eq(t, output.String(), "https://github.com/OWNER/REPO/issues/12\n")
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/issues/12\n", output.String())
|
||||
}
|
||||
|
||||
func TestIssueCreate_recover(t *testing.T) {
|
||||
|
|
@ -152,9 +140,9 @@ func TestIssueCreate_recover(t *testing.T) {
|
|||
"URL": "https://github.com/OWNER/REPO/issues/12"
|
||||
} } } }
|
||||
`, func(inputs map[string]interface{}) {
|
||||
eq(t, inputs["title"], "recovered title")
|
||||
eq(t, inputs["body"], "recovered body")
|
||||
eq(t, inputs["labelIds"], []interface{}{"BUGID", "TODOID"})
|
||||
assert.Equal(t, "recovered title", inputs["title"])
|
||||
assert.Equal(t, "recovered body", inputs["body"])
|
||||
assert.Equal(t, []interface{}{"BUGID", "TODOID"}, inputs["labelIds"])
|
||||
}))
|
||||
|
||||
as, teardown := prompt.InitAskStubber()
|
||||
|
|
@ -201,7 +189,7 @@ func TestIssueCreate_recover(t *testing.T) {
|
|||
t.Errorf("error running command `issue create`: %v", err)
|
||||
}
|
||||
|
||||
eq(t, output.String(), "https://github.com/OWNER/REPO/issues/12\n")
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/issues/12\n", output.String())
|
||||
}
|
||||
|
||||
func TestIssueCreate_nonLegacyTemplate(t *testing.T) {
|
||||
|
|
@ -259,7 +247,7 @@ func TestIssueCreate_nonLegacyTemplate(t *testing.T) {
|
|||
t.Errorf("error running command `issue create`: %v", err)
|
||||
}
|
||||
|
||||
eq(t, output.String(), "https://github.com/OWNER/REPO/issues/12\n")
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/issues/12\n", output.String())
|
||||
}
|
||||
|
||||
func TestIssueCreate_continueInBrowser(t *testing.T) {
|
||||
|
|
@ -294,6 +282,7 @@ func TestIssueCreate_continueInBrowser(t *testing.T) {
|
|||
})
|
||||
|
||||
var seenCmd *exec.Cmd
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmd = cmd
|
||||
return &test.OutputStub{}
|
||||
|
|
@ -376,12 +365,12 @@ func TestIssueCreate_metadata(t *testing.T) {
|
|||
"URL": "https://github.com/OWNER/REPO/issues/12"
|
||||
} } } }
|
||||
`, func(inputs map[string]interface{}) {
|
||||
eq(t, inputs["title"], "TITLE")
|
||||
eq(t, inputs["body"], "BODY")
|
||||
eq(t, inputs["assigneeIds"], []interface{}{"MONAID"})
|
||||
eq(t, inputs["labelIds"], []interface{}{"BUGID", "TODOID"})
|
||||
eq(t, inputs["projectIds"], []interface{}{"ROADMAPID"})
|
||||
eq(t, inputs["milestoneId"], "BIGONEID")
|
||||
assert.Equal(t, "TITLE", inputs["title"])
|
||||
assert.Equal(t, "BODY", inputs["body"])
|
||||
assert.Equal(t, []interface{}{"MONAID"}, inputs["assigneeIds"])
|
||||
assert.Equal(t, []interface{}{"BUGID", "TODOID"}, inputs["labelIds"])
|
||||
assert.Equal(t, []interface{}{"ROADMAPID"}, inputs["projectIds"])
|
||||
assert.Equal(t, "BIGONEID", inputs["milestoneId"])
|
||||
if v, ok := inputs["userIds"]; ok {
|
||||
t.Errorf("did not expect userIds: %v", v)
|
||||
}
|
||||
|
|
@ -395,7 +384,7 @@ func TestIssueCreate_metadata(t *testing.T) {
|
|||
t.Errorf("error running command `issue create`: %v", err)
|
||||
}
|
||||
|
||||
eq(t, output.String(), "https://github.com/OWNER/REPO/issues/12\n")
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/issues/12\n", output.String())
|
||||
}
|
||||
|
||||
func TestIssueCreate_disabledIssues(t *testing.T) {
|
||||
|
|
@ -421,14 +410,24 @@ func TestIssueCreate_web(t *testing.T) {
|
|||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": {
|
||||
"viewer": { "login": "MonaLisa" }
|
||||
} }
|
||||
`),
|
||||
)
|
||||
|
||||
var seenCmd *exec.Cmd
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmd = cmd
|
||||
return &test.OutputStub{}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
output, err := runCommand(http, true, `--web`)
|
||||
output, err := runCommand(http, true, `--web -a @me`)
|
||||
if err != nil {
|
||||
t.Errorf("error running command `issue create`: %v", err)
|
||||
}
|
||||
|
|
@ -437,9 +436,9 @@ func TestIssueCreate_web(t *testing.T) {
|
|||
t.Fatal("expected a command to run")
|
||||
}
|
||||
url := seenCmd.Args[len(seenCmd.Args)-1]
|
||||
eq(t, url, "https://github.com/OWNER/REPO/issues/new")
|
||||
eq(t, output.String(), "")
|
||||
eq(t, output.Stderr(), "Opening github.com/OWNER/REPO/issues/new in your browser.\n")
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/issues/new?assignees=MonaLisa", url)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "Opening github.com/OWNER/REPO/issues/new in your browser.\n", output.Stderr())
|
||||
}
|
||||
|
||||
func TestIssueCreate_webTitleBody(t *testing.T) {
|
||||
|
|
@ -447,6 +446,7 @@ func TestIssueCreate_webTitleBody(t *testing.T) {
|
|||
defer http.Verify(t)
|
||||
|
||||
var seenCmd *exec.Cmd
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmd = cmd
|
||||
return &test.OutputStub{}
|
||||
|
|
@ -462,7 +462,142 @@ func TestIssueCreate_webTitleBody(t *testing.T) {
|
|||
t.Fatal("expected a command to run")
|
||||
}
|
||||
url := strings.ReplaceAll(seenCmd.Args[len(seenCmd.Args)-1], "^", "")
|
||||
eq(t, url, "https://github.com/OWNER/REPO/issues/new?body=mybody&title=mytitle")
|
||||
eq(t, output.String(), "")
|
||||
eq(t, output.Stderr(), "Opening github.com/OWNER/REPO/issues/new in your browser.\n")
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/issues/new?body=mybody&title=mytitle", url)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "Opening github.com/OWNER/REPO/issues/new in your browser.\n", output.Stderr())
|
||||
}
|
||||
|
||||
func TestIssueCreate_webTitleBodyAtMeAssignee(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": {
|
||||
"viewer": { "login": "MonaLisa" }
|
||||
} }
|
||||
`),
|
||||
)
|
||||
|
||||
var seenCmd *exec.Cmd
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmd = cmd
|
||||
return &test.OutputStub{}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
output, err := runCommand(http, true, `-w -t mytitle -b mybody -a @me`)
|
||||
if err != nil {
|
||||
t.Errorf("error running command `issue create`: %v", err)
|
||||
}
|
||||
|
||||
if seenCmd == nil {
|
||||
t.Fatal("expected a command to run")
|
||||
}
|
||||
url := strings.ReplaceAll(seenCmd.Args[len(seenCmd.Args)-1], "^", "")
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/issues/new?assignees=MonaLisa&body=mybody&title=mytitle", url)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "Opening github.com/OWNER/REPO/issues/new in your browser.\n", output.Stderr())
|
||||
}
|
||||
|
||||
func TestIssueCreate_AtMeAssignee(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": {
|
||||
"viewer": { "login": "MonaLisa" }
|
||||
} }
|
||||
`),
|
||||
)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query RepositoryInfo\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"id": "REPOID",
|
||||
"hasIssuesEnabled": true
|
||||
} } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": {
|
||||
"u000": { "login": "MonaLisa", "id": "MONAID" },
|
||||
"u001": { "login": "SomeOneElse", "id": "SOMEID" },
|
||||
"repository": {
|
||||
"l000": { "name": "bug", "id": "BUGID" },
|
||||
"l001": { "name": "TODO", "id": "TODOID" }
|
||||
}
|
||||
} }
|
||||
`),
|
||||
)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation IssueCreate\b`),
|
||||
httpmock.GraphQLMutation(`
|
||||
{ "data": { "createIssue": { "issue": {
|
||||
"URL": "https://github.com/OWNER/REPO/issues/12"
|
||||
} } } }
|
||||
`, func(inputs map[string]interface{}) {
|
||||
assert.Equal(t, "hello", inputs["title"])
|
||||
assert.Equal(t, "cash rules everything around me", inputs["body"])
|
||||
assert.Equal(t, []interface{}{"MONAID", "SOMEID"}, inputs["assigneeIds"])
|
||||
}))
|
||||
|
||||
output, err := runCommand(http, true, `-a @me -a someoneelse -t hello -b "cash rules everything around me"`)
|
||||
if err != nil {
|
||||
t.Errorf("error running command `issue create`: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/issues/12\n", output.String())
|
||||
}
|
||||
|
||||
func TestIssueCreate_webProject(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query RepositoryProjectList\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "projects": {
|
||||
"nodes": [
|
||||
{ "name": "Cleanup", "id": "CLEANUPID", "resourcePath": "/OWNER/REPO/projects/1" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query OrganizationProjectList\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "organization": { "projects": {
|
||||
"nodes": [
|
||||
{ "name": "Triage", "id": "TRIAGEID", "resourcePath": "/orgs/ORG/projects/1" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
|
||||
var seenCmd *exec.Cmd
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmd = cmd
|
||||
return &test.OutputStub{}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
output, err := runCommand(http, true, `-w -t Title -p Cleanup`)
|
||||
if err != nil {
|
||||
t.Errorf("error running command `issue create`: %v", err)
|
||||
}
|
||||
|
||||
if seenCmd == nil {
|
||||
t.Fatal("expected a command to run")
|
||||
}
|
||||
url := strings.ReplaceAll(seenCmd.Args[len(seenCmd.Args)-1], "^", "")
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/issues/new?projects=OWNER%2FREPO%2F1&title=Title", url)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "Opening github.com/OWNER/REPO/issues/new in your browser.\n", output.Stderr())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
issueShared "github.com/cli/cli/pkg/cmd/issue/shared"
|
||||
"github.com/cli/cli/pkg/cmd/pr/shared"
|
||||
prShared "github.com/cli/cli/pkg/cmd/pr/shared"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
|
|
@ -46,6 +47,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
Example: heredoc.Doc(`
|
||||
$ gh issue list -l "help wanted"
|
||||
$ gh issue list -A monalisa
|
||||
$ gh issue list -a @me
|
||||
$ gh issue list --web
|
||||
$ gh issue list --milestone 'MVP'
|
||||
`),
|
||||
|
|
@ -91,15 +93,29 @@ func listRun(opts *ListOptions) error {
|
|||
|
||||
isTerminal := opts.IO.IsStdoutTTY()
|
||||
|
||||
meReplacer := shared.NewMeReplacer(apiClient, baseRepo.RepoHost())
|
||||
filterAssignee, err := meReplacer.Replace(opts.Assignee)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filterAuthor, err := meReplacer.Replace(opts.Author)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filterMention, err := meReplacer.Replace(opts.Mention)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.WebMode {
|
||||
issueListURL := ghrepo.GenerateRepoURL(baseRepo, "issues")
|
||||
openURL, err := prShared.ListURLWithQuery(issueListURL, prShared.FilterOptions{
|
||||
Entity: "issue",
|
||||
State: opts.State,
|
||||
Assignee: opts.Assignee,
|
||||
Assignee: filterAssignee,
|
||||
Labels: opts.Labels,
|
||||
Author: opts.Author,
|
||||
Mention: opts.Mention,
|
||||
Author: filterAuthor,
|
||||
Mention: filterMention,
|
||||
Milestone: opts.Milestone,
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -111,7 +127,7 @@ func listRun(opts *ListOptions) error {
|
|||
return utils.OpenInBrowser(openURL)
|
||||
}
|
||||
|
||||
listResult, err := api.IssueList(apiClient, baseRepo, opts.State, opts.Labels, opts.Assignee, opts.LimitResults, opts.Author, opts.Mention, opts.Milestone)
|
||||
listResult, err := api.IssueList(apiClient, baseRepo, opts.State, opts.Labels, filterAssignee, opts.LimitResults, filterAuthor, filterMention, opts.Milestone)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
"io/ioutil"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
|
|
@ -22,13 +21,6 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func eq(t *testing.T, got interface{}, expected interface{}) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(got, expected) {
|
||||
t.Errorf("expected: %v, got: %v", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, error) {
|
||||
io, _, stdout, stderr := iostreams.Test()
|
||||
io.SetStdoutTTY(isTTY)
|
||||
|
|
@ -79,7 +71,8 @@ func TestIssueList_nontty(t *testing.T) {
|
|||
t.Errorf("error running command `issue list`: %v", err)
|
||||
}
|
||||
|
||||
eq(t, output.Stderr(), "")
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.String(),
|
||||
`1[\t]+number won[\t]+label[\t]+\d+`,
|
||||
`2[\t]+number too[\t]+label[\t]+\d+`,
|
||||
|
|
@ -147,11 +140,36 @@ func TestIssueList_tty_withFlags(t *testing.T) {
|
|||
t.Errorf("error running command `issue list`: %v", err)
|
||||
}
|
||||
|
||||
eq(t, output.Stderr(), "")
|
||||
eq(t, output.String(), `
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
assert.Equal(t, `
|
||||
No issues match your search in OWNER/REPO
|
||||
|
||||
`)
|
||||
`, output.String())
|
||||
}
|
||||
|
||||
func TestIssueList_atMe(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`{"data": {"viewer": {"login": "monalisa"} } }`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query IssueList\b`),
|
||||
httpmock.GraphQLQuery(`
|
||||
{ "data": { "repository": {
|
||||
"hasIssuesEnabled": true,
|
||||
"issues": { "nodes": [] }
|
||||
} } }`, func(_ string, params map[string]interface{}) {
|
||||
assert.Equal(t, "monalisa", params["assignee"].(string))
|
||||
assert.Equal(t, "monalisa", params["author"].(string))
|
||||
assert.Equal(t, "monalisa", params["mention"].(string))
|
||||
}))
|
||||
|
||||
_, err := runCommand(http, true, "-a @me -A @me --mention @me")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `issue list`: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueList_withInvalidLimitFlag(t *testing.T) {
|
||||
|
|
@ -191,8 +209,8 @@ func TestIssueList_nullAssigneeLabels(t *testing.T) {
|
|||
|
||||
_, assigneeDeclared := reqBody.Variables["assignee"]
|
||||
_, labelsDeclared := reqBody.Variables["labels"]
|
||||
eq(t, assigneeDeclared, false)
|
||||
eq(t, labelsDeclared, false)
|
||||
assert.Equal(t, false, assigneeDeclared)
|
||||
assert.Equal(t, false, labelsDeclared)
|
||||
}
|
||||
|
||||
func TestIssueList_disabledIssues(t *testing.T) {
|
||||
|
|
@ -218,6 +236,7 @@ func TestIssueList_web(t *testing.T) {
|
|||
defer http.Verify(t)
|
||||
|
||||
var seenCmd *exec.Cmd
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmd = cmd
|
||||
return &test.OutputStub{}
|
||||
|
|
@ -231,14 +250,14 @@ func TestIssueList_web(t *testing.T) {
|
|||
|
||||
expectedURL := "https://github.com/OWNER/REPO/issues?q=is%3Aissue+assignee%3Apeter+label%3Abug+label%3Adocs+author%3Ajohn+mentions%3Afrank+milestone%3Av1.1"
|
||||
|
||||
eq(t, output.String(), "")
|
||||
eq(t, output.Stderr(), "Opening github.com/OWNER/REPO/issues in your browser.\n")
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "Opening github.com/OWNER/REPO/issues in your browser.\n", output.Stderr())
|
||||
|
||||
if seenCmd == nil {
|
||||
t.Fatal("expected a command to run")
|
||||
}
|
||||
url := seenCmd.Args[len(seenCmd.Args)-1]
|
||||
eq(t, url, expectedURL)
|
||||
assert.Equal(t, expectedURL, url)
|
||||
}
|
||||
|
||||
func TestIssueList_milestoneNotFound(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -46,9 +46,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
|
|||
Display the title, body, and other information about an issue.
|
||||
|
||||
With '--web', open the issue in a web browser instead.
|
||||
`),
|
||||
Example: heredoc.Doc(`
|
||||
`),
|
||||
`),
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
|
|
@ -110,11 +108,11 @@ func viewRun(opts *ViewOptions) error {
|
|||
defer opts.IO.StopPager()
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
return printHumanIssuePreview(opts.IO, issue)
|
||||
return printHumanIssuePreview(opts, issue)
|
||||
}
|
||||
|
||||
if opts.Comments {
|
||||
fmt.Fprint(opts.IO.Out, prShared.RawCommentList(issue.Comments))
|
||||
fmt.Fprint(opts.IO.Out, prShared.RawCommentList(issue.Comments, api.PullRequestReviews{}))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -141,11 +139,11 @@ func printRawIssuePreview(out io.Writer, issue *api.Issue) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func printHumanIssuePreview(io *iostreams.IOStreams, issue *api.Issue) error {
|
||||
out := io.Out
|
||||
func printHumanIssuePreview(opts *ViewOptions, issue *api.Issue) error {
|
||||
out := opts.IO.Out
|
||||
now := time.Now()
|
||||
ago := now.Sub(issue.CreatedAt)
|
||||
cs := io.ColorScheme()
|
||||
cs := opts.IO.ColorScheme()
|
||||
|
||||
// Header (Title and State)
|
||||
fmt.Fprintln(out, cs.Bold(issue.Title))
|
||||
|
|
@ -182,21 +180,23 @@ func printHumanIssuePreview(io *iostreams.IOStreams, issue *api.Issue) error {
|
|||
}
|
||||
|
||||
// Body
|
||||
fmt.Fprintln(out)
|
||||
var md string
|
||||
var err error
|
||||
if issue.Body == "" {
|
||||
issue.Body = "_No description provided_"
|
||||
md = fmt.Sprintf("\n %s\n\n", cs.Gray("No description provided"))
|
||||
} else {
|
||||
style := markdown.GetStyle(opts.IO.TerminalTheme())
|
||||
md, err = markdown.Render(issue.Body, style, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
style := markdown.GetStyle(io.TerminalTheme())
|
||||
md, err := markdown.Render(issue.Body, style, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprint(out, md)
|
||||
fmt.Fprintln(out)
|
||||
fmt.Fprintf(out, "\n%s\n", md)
|
||||
|
||||
// Comments
|
||||
if issue.Comments.TotalCount > 0 {
|
||||
comments, err := prShared.CommentList(io, issue.Comments)
|
||||
preview := !opts.Comments
|
||||
comments, err := prShared.CommentList(opts.IO, issue.Comments, api.PullRequestReviews{}, preview)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import (
|
|||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/briandowns/spinner"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/internal/run"
|
||||
|
|
@ -16,7 +15,6 @@ import (
|
|||
"github.com/cli/cli/pkg/httpmock"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/test"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
|
@ -74,6 +72,7 @@ func TestIssueView_web(t *testing.T) {
|
|||
)
|
||||
|
||||
var seenCmd *exec.Cmd
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmd = cmd
|
||||
return &test.OutputStub{}
|
||||
|
|
@ -110,6 +109,7 @@ func TestIssueView_web_numberArgWithHash(t *testing.T) {
|
|||
)
|
||||
|
||||
var seenCmd *exec.Cmd
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmd = cmd
|
||||
return &test.OutputStub{}
|
||||
|
|
@ -196,6 +196,7 @@ func TestIssueView_nontty_Preview(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.String(), tc.expectedOutputs...)
|
||||
})
|
||||
}
|
||||
|
|
@ -262,6 +263,7 @@ func TestIssueView_tty_Preview(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.String(), tc.expectedOutputs...)
|
||||
})
|
||||
}
|
||||
|
|
@ -281,6 +283,7 @@ func TestIssueView_web_notFound(t *testing.T) {
|
|||
)
|
||||
|
||||
var seenCmd *exec.Cmd
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmd = cmd
|
||||
return &test.OutputStub{}
|
||||
|
|
@ -332,6 +335,7 @@ func TestIssueView_web_urlArg(t *testing.T) {
|
|||
)
|
||||
|
||||
var seenCmd *exec.Cmd
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmd = cmd
|
||||
return &test.OutputStub{}
|
||||
|
|
@ -368,7 +372,7 @@ func TestIssueView_tty_Comments(t *testing.T) {
|
|||
`some title`,
|
||||
`some body`,
|
||||
`———————— Not showing 4 comments ————————`,
|
||||
`marseilles \(collaborator\) • Jan 1, 2020 • Newest comment`,
|
||||
`marseilles \(Collaborator\) • Jan 1, 2020 • Newest comment`,
|
||||
`Comment 5`,
|
||||
`Use --comments to view the full conversation`,
|
||||
`View this issue on GitHub: https://github.com/OWNER/REPO/issues/123`,
|
||||
|
|
@ -383,16 +387,16 @@ func TestIssueView_tty_Comments(t *testing.T) {
|
|||
expectedOutputs: []string{
|
||||
`some title`,
|
||||
`some body`,
|
||||
`monalisa • Jan 1, 2020 • edited`,
|
||||
`monalisa • Jan 1, 2020 • Edited`,
|
||||
`1 \x{1f615} • 2 \x{1f440} • 3 \x{2764}\x{fe0f} • 4 \x{1f389} • 5 \x{1f604} • 6 \x{1f680} • 7 \x{1f44e} • 8 \x{1f44d}`,
|
||||
`Comment 1`,
|
||||
`johnnytest \(contributor\) • Jan 1, 2020`,
|
||||
`johnnytest \(Contributor\) • Jan 1, 2020`,
|
||||
`Comment 2`,
|
||||
`elvisp \(member\) • Jan 1, 2020`,
|
||||
`elvisp \(Member\) • Jan 1, 2020`,
|
||||
`Comment 3`,
|
||||
`loislane \(owner\) • Jan 1, 2020`,
|
||||
`loislane \(Owner\) • Jan 1, 2020`,
|
||||
`Comment 4`,
|
||||
`marseilles \(collaborator\) • Jan 1, 2020 • Newest comment`,
|
||||
`marseilles \(Collaborator\) • Jan 1, 2020 • Newest comment`,
|
||||
`Comment 5`,
|
||||
`View this issue on GitHub: https://github.com/OWNER/REPO/issues/123`,
|
||||
},
|
||||
|
|
@ -404,7 +408,6 @@ func TestIssueView_tty_Comments(t *testing.T) {
|
|||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
stubSpinner()
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
for name, file := range tc.fixtures {
|
||||
|
|
@ -418,6 +421,7 @@ func TestIssueView_tty_Comments(t *testing.T) {
|
|||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.String(), tc.expectedOutputs...)
|
||||
})
|
||||
}
|
||||
|
|
@ -492,12 +496,8 @@ func TestIssueView_nontty_Comments(t *testing.T) {
|
|||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.String(), tc.expectedOutputs...)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func stubSpinner() {
|
||||
utils.StartSpinner = func(_ *spinner.Spinner) {}
|
||||
utils.StopSpinner = func(_ *spinner.Spinner) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ type CheckoutOptions struct {
|
|||
|
||||
SelectorArg string
|
||||
RecurseSubmodules bool
|
||||
Force bool
|
||||
}
|
||||
|
||||
func NewCmdCheckout(f *cmdutil.Factory, runF func(*CheckoutOptions) error) *cobra.Command {
|
||||
|
|
@ -60,7 +61,8 @@ func NewCmdCheckout(f *cmdutil.Factory, runF func(*CheckoutOptions) error) *cobr
|
|||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&opts.RecurseSubmodules, "recurse-submodules", "", false, "Update all active submodules (recursively)")
|
||||
cmd.Flags().BoolVarP(&opts.RecurseSubmodules, "recurse-submodules", "", false, "Update all submodules after checkout")
|
||||
cmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "Reset the existing local branch to the latest state of the pull request")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -116,7 +118,12 @@ func checkoutRun(opts *CheckoutOptions) error {
|
|||
// local branch already exists
|
||||
if _, err := git.ShowRefs("refs/heads/" + newBranchName); err == nil {
|
||||
cmdQueue = append(cmdQueue, []string{"git", "checkout", newBranchName})
|
||||
cmdQueue = append(cmdQueue, []string{"git", "merge", "--ff-only", fmt.Sprintf("refs/remotes/%s", remoteBranch)})
|
||||
if opts.Force {
|
||||
cmdQueue = append(cmdQueue, []string{"git", "reset", "--hard", fmt.Sprintf("refs/remotes/%s", remoteBranch)})
|
||||
} else {
|
||||
// TODO: check if non-fast-forward and suggest to use `--force`
|
||||
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})
|
||||
|
|
@ -139,11 +146,24 @@ func checkoutRun(opts *CheckoutOptions) error {
|
|||
ref := fmt.Sprintf("refs/pull/%d/head", pr.Number)
|
||||
if newBranchName == currentBranch {
|
||||
// PR head matches currently checked out branch
|
||||
|
||||
cmdQueue = append(cmdQueue, []string{"git", "fetch", baseURLOrName, ref})
|
||||
cmdQueue = append(cmdQueue, []string{"git", "merge", "--ff-only", "FETCH_HEAD"})
|
||||
|
||||
if opts.Force {
|
||||
cmdQueue = append(cmdQueue, []string{"git", "reset", "--hard", "FETCH_HEAD"})
|
||||
} else {
|
||||
// TODO: check if non-fast-forward and suggest to use `--force`
|
||||
cmdQueue = append(cmdQueue, []string{"git", "merge", "--ff-only", "FETCH_HEAD"})
|
||||
}
|
||||
} else {
|
||||
// create a new branch
|
||||
cmdQueue = append(cmdQueue, []string{"git", "fetch", baseURLOrName, fmt.Sprintf("%s:%s", ref, newBranchName)})
|
||||
|
||||
if opts.Force {
|
||||
cmdQueue = append(cmdQueue, []string{"git", "fetch", baseURLOrName, fmt.Sprintf("%s:%s", ref, newBranchName), "--force"})
|
||||
} else {
|
||||
// TODO: check if non-fast-forward and suggest to use `--force`
|
||||
cmdQueue = append(cmdQueue, []string{"git", "fetch", baseURLOrName, fmt.Sprintf("%s:%s", ref, newBranchName)})
|
||||
}
|
||||
cmdQueue = append(cmdQueue, []string{"git", "checkout", newBranchName})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import (
|
|||
"io/ioutil"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
|
|
@ -25,13 +24,6 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func eq(t *testing.T, got interface{}, expected interface{}) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(got, expected) {
|
||||
t.Errorf("expected: %v, got: %v", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
type errorStub struct {
|
||||
message string
|
||||
}
|
||||
|
|
@ -117,6 +109,7 @@ func TestPRCheckout_sameRepo(t *testing.T) {
|
|||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git show-ref --verify -- refs/heads/feature":
|
||||
|
|
@ -137,10 +130,10 @@ func TestPRCheckout_sameRepo(t *testing.T) {
|
|||
if !assert.Equal(t, 4, len(ranCommands)) {
|
||||
return
|
||||
}
|
||||
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")
|
||||
assert.Equal(t, "git fetch origin +refs/heads/feature:refs/remotes/origin/feature", strings.Join(ranCommands[0], " "))
|
||||
assert.Equal(t, "git checkout -b feature --no-track origin/feature", strings.Join(ranCommands[1], " "))
|
||||
assert.Equal(t, "git config branch.feature.remote origin", strings.Join(ranCommands[2], " "))
|
||||
assert.Equal(t, "git config branch.feature.merge refs/heads/feature", strings.Join(ranCommands[3], " "))
|
||||
}
|
||||
|
||||
func TestPRCheckout_urlArg(t *testing.T) {
|
||||
|
|
@ -162,6 +155,7 @@ func TestPRCheckout_urlArg(t *testing.T) {
|
|||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git show-ref --verify -- refs/heads/feature":
|
||||
|
|
@ -174,11 +168,11 @@ func TestPRCheckout_urlArg(t *testing.T) {
|
|||
defer restoreCmd()
|
||||
|
||||
output, err := runCommand(http, nil, "master", `https://github.com/OWNER/REPO/pull/123/files`)
|
||||
eq(t, err, nil)
|
||||
eq(t, output.String(), "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
eq(t, len(ranCommands), 4)
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git checkout -b feature --no-track origin/feature")
|
||||
assert.Equal(t, 4, len(ranCommands))
|
||||
assert.Equal(t, "git checkout -b feature --no-track origin/feature", strings.Join(ranCommands[1], " "))
|
||||
}
|
||||
|
||||
func TestPRCheckout_urlArg_differentBase(t *testing.T) {
|
||||
|
|
@ -201,6 +195,7 @@ func TestPRCheckout_urlArg_differentBase(t *testing.T) {
|
|||
http.StubRepoInfoResponse("OWNER", "REPO", "master")
|
||||
|
||||
ranCommands := [][]string{}
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git show-ref --verify -- refs/heads/feature":
|
||||
|
|
@ -213,8 +208,8 @@ func TestPRCheckout_urlArg_differentBase(t *testing.T) {
|
|||
defer restoreCmd()
|
||||
|
||||
output, err := runCommand(http, nil, "master", `https://github.com/OTHER/POE/pull/123/files`)
|
||||
eq(t, err, nil)
|
||||
eq(t, output.String(), "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
bodyBytes, _ := ioutil.ReadAll(http.Requests[0].Body)
|
||||
reqBody := struct {
|
||||
|
|
@ -225,12 +220,12 @@ func TestPRCheckout_urlArg_differentBase(t *testing.T) {
|
|||
}{}
|
||||
_ = json.Unmarshal(bodyBytes, &reqBody)
|
||||
|
||||
eq(t, reqBody.Variables.Owner, "OTHER")
|
||||
eq(t, reqBody.Variables.Repo, "POE")
|
||||
assert.Equal(t, "OTHER", reqBody.Variables.Owner)
|
||||
assert.Equal(t, "POE", reqBody.Variables.Repo)
|
||||
|
||||
eq(t, len(ranCommands), 5)
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git fetch https://github.com/OTHER/POE.git refs/pull/123/head:feature")
|
||||
eq(t, strings.Join(ranCommands[3], " "), "git config branch.feature.remote https://github.com/OTHER/POE.git")
|
||||
assert.Equal(t, 5, len(ranCommands))
|
||||
assert.Equal(t, "git fetch https://github.com/OTHER/POE.git refs/pull/123/head:feature", strings.Join(ranCommands[1], " "))
|
||||
assert.Equal(t, "git config branch.feature.remote https://github.com/OTHER/POE.git", strings.Join(ranCommands[3], " "))
|
||||
}
|
||||
|
||||
func TestPRCheckout_branchArg(t *testing.T) {
|
||||
|
|
@ -253,6 +248,7 @@ func TestPRCheckout_branchArg(t *testing.T) {
|
|||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git show-ref --verify -- refs/heads/feature":
|
||||
|
|
@ -265,11 +261,11 @@ func TestPRCheckout_branchArg(t *testing.T) {
|
|||
defer restoreCmd()
|
||||
|
||||
output, err := runCommand(http, nil, "master", `hubot:feature`)
|
||||
eq(t, err, nil)
|
||||
eq(t, output.String(), "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
eq(t, len(ranCommands), 5)
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git fetch origin refs/pull/123/head:feature")
|
||||
assert.Equal(t, 5, len(ranCommands))
|
||||
assert.Equal(t, "git fetch origin refs/pull/123/head:feature", strings.Join(ranCommands[1], " "))
|
||||
}
|
||||
|
||||
func TestPRCheckout_existingBranch(t *testing.T) {
|
||||
|
|
@ -292,6 +288,7 @@ func TestPRCheckout_existingBranch(t *testing.T) {
|
|||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git show-ref --verify -- refs/heads/feature":
|
||||
|
|
@ -304,13 +301,13 @@ func TestPRCheckout_existingBranch(t *testing.T) {
|
|||
defer restoreCmd()
|
||||
|
||||
output, err := runCommand(http, nil, "master", `123`)
|
||||
eq(t, err, nil)
|
||||
eq(t, output.String(), "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
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")
|
||||
assert.Equal(t, 3, len(ranCommands))
|
||||
assert.Equal(t, "git fetch origin +refs/heads/feature:refs/remotes/origin/feature", strings.Join(ranCommands[0], " "))
|
||||
assert.Equal(t, "git checkout feature", strings.Join(ranCommands[1], " "))
|
||||
assert.Equal(t, "git merge --ff-only refs/remotes/origin/feature", strings.Join(ranCommands[2], " "))
|
||||
}
|
||||
|
||||
func TestPRCheckout_differentRepo_remoteExists(t *testing.T) {
|
||||
|
|
@ -344,6 +341,7 @@ func TestPRCheckout_differentRepo_remoteExists(t *testing.T) {
|
|||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git show-ref --verify -- refs/heads/feature":
|
||||
|
|
@ -356,14 +354,14 @@ func TestPRCheckout_differentRepo_remoteExists(t *testing.T) {
|
|||
defer restoreCmd()
|
||||
|
||||
output, err := runCommand(http, remotes, "master", `123`)
|
||||
eq(t, err, nil)
|
||||
eq(t, output.String(), "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
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")
|
||||
assert.Equal(t, 4, len(ranCommands))
|
||||
assert.Equal(t, "git fetch robot-fork +refs/heads/feature:refs/remotes/robot-fork/feature", strings.Join(ranCommands[0], " "))
|
||||
assert.Equal(t, "git checkout -b feature --no-track robot-fork/feature", strings.Join(ranCommands[1], " "))
|
||||
assert.Equal(t, "git config branch.feature.remote robot-fork", strings.Join(ranCommands[2], " "))
|
||||
assert.Equal(t, "git config branch.feature.merge refs/heads/feature", strings.Join(ranCommands[3], " "))
|
||||
}
|
||||
|
||||
func TestPRCheckout_differentRepo(t *testing.T) {
|
||||
|
|
@ -386,6 +384,7 @@ func TestPRCheckout_differentRepo(t *testing.T) {
|
|||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git config branch.feature.merge":
|
||||
|
|
@ -398,14 +397,14 @@ func TestPRCheckout_differentRepo(t *testing.T) {
|
|||
defer restoreCmd()
|
||||
|
||||
output, err := runCommand(http, nil, "master", `123`)
|
||||
eq(t, err, nil)
|
||||
eq(t, output.String(), "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
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")
|
||||
assert.Equal(t, 4, len(ranCommands))
|
||||
assert.Equal(t, "git fetch origin refs/pull/123/head:feature", strings.Join(ranCommands[0], " "))
|
||||
assert.Equal(t, "git checkout feature", strings.Join(ranCommands[1], " "))
|
||||
assert.Equal(t, "git config branch.feature.remote origin", strings.Join(ranCommands[2], " "))
|
||||
assert.Equal(t, "git config branch.feature.merge refs/pull/123/head", strings.Join(ranCommands[3], " "))
|
||||
}
|
||||
|
||||
func TestPRCheckout_differentRepo_existingBranch(t *testing.T) {
|
||||
|
|
@ -428,6 +427,7 @@ func TestPRCheckout_differentRepo_existingBranch(t *testing.T) {
|
|||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git config branch.feature.merge":
|
||||
|
|
@ -440,12 +440,12 @@ func TestPRCheckout_differentRepo_existingBranch(t *testing.T) {
|
|||
defer restoreCmd()
|
||||
|
||||
output, err := runCommand(http, nil, "master", `123`)
|
||||
eq(t, err, nil)
|
||||
eq(t, output.String(), "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
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")
|
||||
assert.Equal(t, 2, len(ranCommands))
|
||||
assert.Equal(t, "git fetch origin refs/pull/123/head:feature", strings.Join(ranCommands[0], " "))
|
||||
assert.Equal(t, "git checkout feature", strings.Join(ranCommands[1], " "))
|
||||
}
|
||||
|
||||
func TestPRCheckout_detachedHead(t *testing.T) {
|
||||
|
|
@ -468,6 +468,7 @@ func TestPRCheckout_detachedHead(t *testing.T) {
|
|||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git config branch.feature.merge":
|
||||
|
|
@ -480,12 +481,12 @@ func TestPRCheckout_detachedHead(t *testing.T) {
|
|||
defer restoreCmd()
|
||||
|
||||
output, err := runCommand(http, nil, "", `123`)
|
||||
eq(t, err, nil)
|
||||
eq(t, output.String(), "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
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")
|
||||
assert.Equal(t, 2, len(ranCommands))
|
||||
assert.Equal(t, "git fetch origin refs/pull/123/head:feature", strings.Join(ranCommands[0], " "))
|
||||
assert.Equal(t, "git checkout feature", strings.Join(ranCommands[1], " "))
|
||||
}
|
||||
|
||||
func TestPRCheckout_differentRepo_currentBranch(t *testing.T) {
|
||||
|
|
@ -508,6 +509,7 @@ func TestPRCheckout_differentRepo_currentBranch(t *testing.T) {
|
|||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git config branch.feature.merge":
|
||||
|
|
@ -520,12 +522,12 @@ func TestPRCheckout_differentRepo_currentBranch(t *testing.T) {
|
|||
defer restoreCmd()
|
||||
|
||||
output, err := runCommand(http, nil, "feature", `123`)
|
||||
eq(t, err, nil)
|
||||
eq(t, output.String(), "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
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")
|
||||
assert.Equal(t, 2, len(ranCommands))
|
||||
assert.Equal(t, "git fetch origin refs/pull/123/head", strings.Join(ranCommands[0], " "))
|
||||
assert.Equal(t, "git merge --ff-only FETCH_HEAD", strings.Join(ranCommands[1], " "))
|
||||
}
|
||||
|
||||
func TestPRCheckout_differentRepo_invalidBranchName(t *testing.T) {
|
||||
|
|
@ -547,6 +549,7 @@ func TestPRCheckout_differentRepo_invalidBranchName(t *testing.T) {
|
|||
} } } }
|
||||
`))
|
||||
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
t.Errorf("unexpected external invocation: %v", cmd.Args)
|
||||
return &test.OutputStub{}
|
||||
|
|
@ -554,9 +557,7 @@ func TestPRCheckout_differentRepo_invalidBranchName(t *testing.T) {
|
|||
defer restoreCmd()
|
||||
|
||||
output, err := runCommand(http, nil, "master", `123`)
|
||||
if assert.Errorf(t, err, "expected command to fail") {
|
||||
assert.Equal(t, `invalid branch name: "-foo"`, err.Error())
|
||||
}
|
||||
assert.EqualError(t, err, `invalid branch name: "-foo"`)
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
}
|
||||
|
||||
|
|
@ -580,6 +581,7 @@ func TestPRCheckout_maintainerCanModify(t *testing.T) {
|
|||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git config branch.feature.merge":
|
||||
|
|
@ -592,14 +594,14 @@ func TestPRCheckout_maintainerCanModify(t *testing.T) {
|
|||
defer restoreCmd()
|
||||
|
||||
output, err := runCommand(http, nil, "master", `123`)
|
||||
eq(t, err, nil)
|
||||
eq(t, output.String(), "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
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")
|
||||
assert.Equal(t, 4, len(ranCommands))
|
||||
assert.Equal(t, "git fetch origin refs/pull/123/head:feature", strings.Join(ranCommands[0], " "))
|
||||
assert.Equal(t, "git checkout feature", strings.Join(ranCommands[1], " "))
|
||||
assert.Equal(t, "git config branch.feature.remote https://github.com/hubot/REPO.git", strings.Join(ranCommands[2], " "))
|
||||
assert.Equal(t, "git config branch.feature.merge refs/heads/feature", strings.Join(ranCommands[3], " "))
|
||||
}
|
||||
|
||||
func TestPRCheckout_recurseSubmodules(t *testing.T) {
|
||||
|
|
@ -621,6 +623,7 @@ func TestPRCheckout_recurseSubmodules(t *testing.T) {
|
|||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git show-ref --verify -- refs/heads/feature":
|
||||
|
|
@ -633,13 +636,55 @@ func TestPRCheckout_recurseSubmodules(t *testing.T) {
|
|||
defer restoreCmd()
|
||||
|
||||
output, err := runCommand(http, nil, "master", `123 --recurse-submodules`)
|
||||
eq(t, err, nil)
|
||||
eq(t, output.String(), "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
eq(t, len(ranCommands), 5)
|
||||
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")
|
||||
eq(t, strings.Join(ranCommands[3], " "), "git submodule sync --recursive")
|
||||
eq(t, strings.Join(ranCommands[4], " "), "git submodule update --init --recursive")
|
||||
assert.Equal(t, 5, len(ranCommands))
|
||||
assert.Equal(t, "git fetch origin +refs/heads/feature:refs/remotes/origin/feature", strings.Join(ranCommands[0], " "))
|
||||
assert.Equal(t, "git checkout feature", strings.Join(ranCommands[1], " "))
|
||||
assert.Equal(t, "git merge --ff-only refs/remotes/origin/feature", strings.Join(ranCommands[2], " "))
|
||||
assert.Equal(t, "git submodule sync --recursive", strings.Join(ranCommands[3], " "))
|
||||
assert.Equal(t, "git submodule update --init --recursive", strings.Join(ranCommands[4], " "))
|
||||
}
|
||||
|
||||
func TestPRCheckout_force(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
|
||||
http.Register(httpmock.GraphQL(`query PullRequestByNumber\b`), httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"number": 123,
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO"
|
||||
},
|
||||
"isCrossRepository": false,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git show-ref --verify -- refs/heads/feature":
|
||||
return &test.OutputStub{}
|
||||
default:
|
||||
ranCommands = append(ranCommands, cmd.Args)
|
||||
return &test.OutputStub{}
|
||||
}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
output, err := runCommand(http, nil, "master", `123 --force`)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
assert.Equal(t, len(ranCommands), 3)
|
||||
assert.Equal(t, "git fetch origin +refs/heads/feature:refs/remotes/origin/feature", strings.Join(ranCommands[0], " "))
|
||||
assert.Equal(t, "git checkout feature", strings.Join(ranCommands[1], " "))
|
||||
assert.Equal(t, "git reset --hard refs/remotes/origin/feature", strings.Join(ranCommands[2], " "))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/internal/run"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/test"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
|
@ -195,10 +195,10 @@ func Test_checksRun(t *testing.T) {
|
|||
}
|
||||
|
||||
err := checksRun(opts)
|
||||
if err != nil {
|
||||
assert.Equal(t, tt.wantErr, err.Error())
|
||||
} else if tt.wantErr != "" {
|
||||
t.Errorf("expected %q, got nil error", tt.wantErr)
|
||||
if tt.wantErr != "" {
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.wantOut, stdout.String())
|
||||
|
|
@ -252,10 +252,9 @@ func TestChecksRun_web(t *testing.T) {
|
|||
|
||||
opts.IO = io
|
||||
|
||||
cs, teardown := test.InitCmdStubber()
|
||||
defer teardown()
|
||||
|
||||
cs.Stub("") // browser open
|
||||
cs, teardown := run.Stub()
|
||||
defer teardown(t)
|
||||
cs.Register(`https://github\.com/OWNER/REPO/pull/123/checks$`, 0, "")
|
||||
|
||||
err := checksRun(opts)
|
||||
assert.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/internal/run"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
|
|
@ -135,18 +136,17 @@ func TestPrClose_deleteBranch(t *testing.T) {
|
|||
httpmock.REST("DELETE", "repos/OWNER/REPO/git/refs/heads/blueberries"),
|
||||
httpmock.StringResponse(`{}`))
|
||||
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
cs.Stub("") // git config --get-regexp ^branch\.blueberries\.(remote|merge)$
|
||||
cs.Stub("") // git rev-parse --verify blueberries`
|
||||
cs.Stub("") // git branch -d
|
||||
cs.Stub("") // git push origin --delete blueberries
|
||||
cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "")
|
||||
cs.Register(`git branch -D blueberries`, 0, "")
|
||||
|
||||
output, err := runCommand(http, true, `96 --delete-branch`)
|
||||
if err != nil {
|
||||
t.Fatalf("Got unexpected error running `pr close` %s", err)
|
||||
}
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), `Closed pull request #96 \(The title of the PR\)`, `Deleted branch blueberries`)
|
||||
}
|
||||
|
|
|
|||
78
pkg/cmd/pr/comment/comment.go
Normal file
78
pkg/cmd/pr/comment/comment.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package comment
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/cmd/pr/shared"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewCmdComment(f *cmdutil.Factory, runF func(*shared.CommentableOptions) error) *cobra.Command {
|
||||
opts := &shared.CommentableOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
EditSurvey: shared.CommentableEditSurvey(f.Config, f.IOStreams),
|
||||
InteractiveEditSurvey: shared.CommentableInteractiveEditSurvey(f.Config, f.IOStreams),
|
||||
ConfirmSubmitSurvey: shared.CommentableConfirmSubmitSurvey,
|
||||
OpenInBrowser: utils.OpenInBrowser,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "comment [<number> | <url> | <branch>]",
|
||||
Short: "Create a new pr comment",
|
||||
Example: heredoc.Doc(`
|
||||
$ gh pr comment 22 --body "This looks great, lets get it deployed."
|
||||
`),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 {
|
||||
return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")}
|
||||
}
|
||||
var selector string
|
||||
if len(args) > 0 {
|
||||
selector = args[0]
|
||||
}
|
||||
opts.RetrieveCommentable = retrievePR(f.HttpClient, f.BaseRepo, f.Branch, f.Remotes, selector)
|
||||
return shared.CommentablePreRun(cmd, opts)
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
return shared.CommentableRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "Supply a body. Will prompt for one otherwise.")
|
||||
cmd.Flags().BoolP("editor", "e", false, "Add body using editor")
|
||||
cmd.Flags().BoolP("web", "w", false, "Add body in browser")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func retrievePR(httpClient func() (*http.Client, error),
|
||||
baseRepo func() (ghrepo.Interface, error),
|
||||
branch func() (string, error),
|
||||
remotes func() (context.Remotes, error),
|
||||
selector string) func() (shared.Commentable, ghrepo.Interface, error) {
|
||||
return func() (shared.Commentable, ghrepo.Interface, error) {
|
||||
httpClient, err := httpClient()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
pr, repo, err := shared.PRFromArgs(apiClient, baseRepo, branch, remotes, selector)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return pr, repo, nil
|
||||
}
|
||||
}
|
||||
278
pkg/cmd/pr/comment/comment_test.go
Normal file
278
pkg/cmd/pr/comment/comment_test.go
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
package comment
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/cmd/pr/shared"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewCmdComment(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
output shared.CommentableOptions
|
||||
wantsErr bool
|
||||
}{
|
||||
{
|
||||
name: "no arguments",
|
||||
input: "",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
Body: "",
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "pr number",
|
||||
input: "1",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
Body: "",
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "pr url",
|
||||
input: "https://github.com/OWNER/REPO/pull/12",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
Body: "",
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "pr branch",
|
||||
input: "branch-name",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
Body: "",
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "body flag",
|
||||
input: "1 --body test",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeInline,
|
||||
Body: "test",
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "editor flag",
|
||||
input: "1 --editor",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeEditor,
|
||||
Body: "",
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "web flag",
|
||||
input: "1 --web",
|
||||
output: shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeWeb,
|
||||
Body: "",
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "editor and web flags",
|
||||
input: "1 --editor --web",
|
||||
output: shared.CommentableOptions{},
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "editor and body flags",
|
||||
input: "1 --editor --body test",
|
||||
output: shared.CommentableOptions{},
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "web and body flags",
|
||||
input: "1 --web --body test",
|
||||
output: shared.CommentableOptions{},
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "editor, web, and body flags",
|
||||
input: "1 --editor --web --body test",
|
||||
output: shared.CommentableOptions{},
|
||||
wantsErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
io, _, _, _ := iostreams.Test()
|
||||
io.SetStdoutTTY(true)
|
||||
io.SetStdinTTY(true)
|
||||
io.SetStderrTTY(true)
|
||||
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: io,
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.input)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts *shared.CommentableOptions
|
||||
cmd := NewCmdComment(f, func(opts *shared.CommentableOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
cmd.Flags().BoolP("help", "x", false, "")
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(&bytes.Buffer{})
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.output.Interactive, gotOpts.Interactive)
|
||||
assert.Equal(t, tt.output.InputType, gotOpts.InputType)
|
||||
assert.Equal(t, tt.output.Body, gotOpts.Body)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_commentRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input *shared.CommentableOptions
|
||||
httpStubs func(*testing.T, *httpmock.Registry)
|
||||
stdout string
|
||||
stderr string
|
||||
}{
|
||||
{
|
||||
name: "interactive editor",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: true,
|
||||
InputType: 0,
|
||||
Body: "",
|
||||
|
||||
InteractiveEditSurvey: func() (string, error) { return "comment body", nil },
|
||||
ConfirmSubmitSurvey: func() (bool, error) { return true, nil },
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockPullRequestFromNumber(t, reg)
|
||||
mockCommentCreate(t, reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-456\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive web",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeWeb,
|
||||
Body: "",
|
||||
|
||||
OpenInBrowser: func(string) error { return nil },
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockPullRequestFromNumber(t, reg)
|
||||
},
|
||||
stderr: "Opening github.com/OWNER/REPO/pull/123 in your browser.\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive editor",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeEditor,
|
||||
Body: "",
|
||||
|
||||
EditSurvey: func() (string, error) { return "comment body", nil },
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockPullRequestFromNumber(t, reg)
|
||||
mockCommentCreate(t, reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-456\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive inline",
|
||||
input: &shared.CommentableOptions{
|
||||
Interactive: false,
|
||||
InputType: shared.InputTypeInline,
|
||||
Body: "comment body",
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockPullRequestFromNumber(t, reg)
|
||||
mockCommentCreate(t, reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/pull/123#issuecomment-456\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
io, _, stdout, stderr := iostreams.Test()
|
||||
io.SetStdoutTTY(true)
|
||||
io.SetStdinTTY(true)
|
||||
io.SetStderrTTY(true)
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
tt.httpStubs(t, reg)
|
||||
|
||||
httpClient := func() (*http.Client, error) { return &http.Client{Transport: reg}, nil }
|
||||
baseRepo := func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil }
|
||||
branch := func() (string, error) { return "", nil }
|
||||
remotes := func() (context.Remotes, error) { return nil, nil }
|
||||
|
||||
tt.input.IO = io
|
||||
tt.input.HttpClient = httpClient
|
||||
tt.input.RetrieveCommentable = retrievePR(httpClient, baseRepo, branch, remotes, "123")
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := shared.CommentableRun(tt.input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.stdout, stdout.String())
|
||||
assert.Equal(t, tt.stderr, stderr.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mockPullRequestFromNumber(_ *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"number": 123,
|
||||
"url": "https://github.com/OWNER/REPO/pull/123"
|
||||
} } } }`),
|
||||
)
|
||||
}
|
||||
|
||||
func mockCommentCreate(t *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`mutation CommentCreate\b`),
|
||||
httpmock.GraphQLMutation(`
|
||||
{ "data": { "addComment": { "commentEdge": { "node": {
|
||||
"url": "https://github.com/OWNER/REPO/pull/123#issuecomment-456"
|
||||
} } } } }`,
|
||||
func(inputs map[string]interface{}) {
|
||||
assert.Equal(t, "comment body", inputs["body"])
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
|
@ -53,13 +53,15 @@ type CreateOptions struct {
|
|||
Labels []string
|
||||
Projects []string
|
||||
Milestone string
|
||||
|
||||
MaintainerCanModify bool
|
||||
}
|
||||
|
||||
type CreateContext struct {
|
||||
// This struct stores contextual data about the creation process and is for building up enough
|
||||
// data to create a pull request
|
||||
RepoContext *context.ResolvedRemotes
|
||||
BaseRepo ghrepo.Interface
|
||||
BaseRepo *api.Repository
|
||||
HeadRepo ghrepo.Interface
|
||||
BaseTrackingBranch string
|
||||
BaseBranch string
|
||||
|
|
@ -91,6 +93,10 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
|
||||
A prompt will also ask for the title and the body of the pull request. Use '--title'
|
||||
and '--body' to skip this, or use '--fill' to autofill these values from git commits.
|
||||
|
||||
By default users with write access to the base respository can add new commits to your branch.
|
||||
If undesired, you may disable access of maintainers by using '--no-maintainer-edit'
|
||||
You can always change this setting later via the web interface.
|
||||
`),
|
||||
Example: heredoc.Doc(`
|
||||
$ gh pr create --title "The bug is fixed" --body "Everything works again"
|
||||
|
|
@ -103,6 +109,8 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
opts.TitleProvided = cmd.Flags().Changed("title")
|
||||
opts.BodyProvided = cmd.Flags().Changed("body")
|
||||
opts.RepoOverride, _ = cmd.Flags().GetString("repo")
|
||||
noMaintainerEdit, _ := cmd.Flags().GetBool("no-maintainer-edit")
|
||||
opts.MaintainerCanModify = !noMaintainerEdit
|
||||
|
||||
if !opts.IO.CanPrompt() && opts.RecoverFile != "" {
|
||||
return &cmdutil.FlagError{Err: errors.New("--recover only supported when running interactively")}
|
||||
|
|
@ -118,6 +126,9 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
if len(opts.Reviewers) > 0 && opts.WebMode {
|
||||
return errors.New("the --reviewer flag is not supported with --web")
|
||||
}
|
||||
if cmd.Flags().Changed("no-maintainer-edit") && opts.WebMode {
|
||||
return errors.New("the --no-maintainer-edit flag is not supported with --web")
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
|
|
@ -135,10 +146,11 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
fl.BoolVarP(&opts.WebMode, "web", "w", false, "Open the web browser to create a pull request")
|
||||
fl.BoolVarP(&opts.Autofill, "fill", "f", false, "Do not prompt for title/body and just use commit info")
|
||||
fl.StringSliceVarP(&opts.Reviewers, "reviewer", "r", nil, "Request reviews from people or teams by their `handle`")
|
||||
fl.StringSliceVarP(&opts.Assignees, "assignee", "a", nil, "Assign people by their `login`")
|
||||
fl.StringSliceVarP(&opts.Assignees, "assignee", "a", nil, "Assign people by their `login`. Use \"@me\" to self-assign.")
|
||||
fl.StringSliceVarP(&opts.Labels, "label", "l", nil, "Add labels by `name`")
|
||||
fl.StringSliceVarP(&opts.Projects, "project", "p", nil, "Add the pull request to projects by `name`")
|
||||
fl.StringVarP(&opts.Milestone, "milestone", "m", "", "Add the pull request to a milestone by `name`")
|
||||
fl.Bool("no-maintainer-edit", false, "Disable maintainer's ability to modify pull request")
|
||||
fl.StringVar(&opts.RecoverFile, "recover", "", "Recover input from a failed run of create")
|
||||
|
||||
return cmd
|
||||
|
|
@ -252,7 +264,7 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
allowMetadata := ctx.BaseRepo.(*api.Repository).ViewerCanTriage()
|
||||
allowMetadata := ctx.BaseRepo.ViewerCanTriage()
|
||||
action, err := shared.ConfirmSubmission(!state.HasMetadata(), allowMetadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to confirm: %w", err)
|
||||
|
|
@ -374,10 +386,16 @@ func NewIssueState(ctx CreateContext, opts CreateOptions) (*shared.IssueMetadata
|
|||
milestoneTitles = []string{opts.Milestone}
|
||||
}
|
||||
|
||||
meReplacer := shared.NewMeReplacer(ctx.Client, ctx.BaseRepo.RepoHost())
|
||||
assignees, err := meReplacer.ReplaceSlice(opts.Assignees)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
state := &shared.IssueMetadataState{
|
||||
Type: shared.PRMetadata,
|
||||
Reviewers: opts.Reviewers,
|
||||
Assignees: opts.Assignees,
|
||||
Assignees: assignees,
|
||||
Labels: opts.Labels,
|
||||
Projects: opts.Projects,
|
||||
Milestones: milestoneTitles,
|
||||
|
|
@ -561,11 +579,12 @@ func submitPR(opts CreateOptions, ctx CreateContext, state shared.IssueMetadataS
|
|||
client := ctx.Client
|
||||
|
||||
params := map[string]interface{}{
|
||||
"title": state.Title,
|
||||
"body": state.Body,
|
||||
"draft": state.Draft,
|
||||
"baseRefName": ctx.BaseBranch,
|
||||
"headRefName": ctx.HeadBranchLabel,
|
||||
"title": state.Title,
|
||||
"body": state.Body,
|
||||
"draft": state.Draft,
|
||||
"baseRefName": ctx.BaseBranch,
|
||||
"headRefName": ctx.HeadBranchLabel,
|
||||
"maintainerCanModify": opts.MaintainerCanModify,
|
||||
}
|
||||
|
||||
if params["title"] == "" {
|
||||
|
|
@ -577,7 +596,9 @@ func submitPR(opts CreateOptions, ctx CreateContext, state shared.IssueMetadataS
|
|||
return err
|
||||
}
|
||||
|
||||
pr, err := api.CreatePullRequest(client, ctx.BaseRepo.(*api.Repository), params)
|
||||
opts.IO.StartProgressIndicator()
|
||||
pr, err := api.CreatePullRequest(client, ctx.BaseRepo, params)
|
||||
opts.IO.StopProgressIndicator()
|
||||
if pr != nil {
|
||||
fmt.Fprintln(opts.IO.Out, pr.URL)
|
||||
}
|
||||
|
|
@ -591,6 +612,14 @@ func submitPR(opts CreateOptions, ctx CreateContext, state shared.IssueMetadataS
|
|||
}
|
||||
|
||||
func previewPR(opts CreateOptions, ctx CreateContext, state shared.IssueMetadataState) error {
|
||||
if len(state.Projects) > 0 {
|
||||
var err error
|
||||
state.Projects, err = api.ProjectNamesToPaths(ctx.Client, ctx.BaseRepo, state.Projects)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not add to project: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
openURL, err := generateCompareURL(ctx, state)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -613,7 +642,9 @@ func handlePush(opts CreateOptions, ctx CreateContext) error {
|
|||
// if a head repository could not be determined so far, automatically create
|
||||
// one by forking the base repository
|
||||
if headRepo == nil && ctx.IsPushEnabled {
|
||||
opts.IO.StartProgressIndicator()
|
||||
headRepo, err = api.ForkRepo(client, ctx.BaseRepo)
|
||||
opts.IO.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error forking repo: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ import (
|
|||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/config"
|
||||
|
|
@ -28,13 +28,6 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func eq(t *testing.T, got interface{}, expected interface{}) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(got, expected) {
|
||||
t.Errorf("expected: %v, got: %v", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func runCommand(rt http.RoundTripper, remotes context.Remotes, branch string, isTTY bool, cli string) (*test.CmdOut, error) {
|
||||
return runCommandWithRootDirOverridden(rt, remotes, branch, isTTY, cli, "")
|
||||
}
|
||||
|
|
@ -105,6 +98,7 @@ func TestPRCreate_nontty_web(t *testing.T) {
|
|||
|
||||
http.StubRepoInfoResponse("OWNER", "REPO", "master")
|
||||
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
|
||||
|
|
@ -115,12 +109,12 @@ func TestPRCreate_nontty_web(t *testing.T) {
|
|||
output, err := runCommand(http, nil, "feature", false, `--web --head=feature`)
|
||||
require.NoError(t, err)
|
||||
|
||||
eq(t, output.String(), "")
|
||||
eq(t, output.Stderr(), "")
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
|
||||
eq(t, len(cs.Calls), 3)
|
||||
assert.Equal(t, 3, len(cs.Calls))
|
||||
browserCall := cs.Calls[2].Args
|
||||
eq(t, browserCall[len(browserCall)-1], "https://github.com/OWNER/REPO/compare/master...feature?expand=1")
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/compare/master...feature?expand=1", browserCall[len(browserCall)-1])
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -129,11 +123,7 @@ func TestPRCreate_nontty_insufficient_flags(t *testing.T) {
|
|||
defer http.Verify(t)
|
||||
|
||||
output, err := runCommand(http, nil, "feature", false, "")
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
|
||||
assert.Equal(t, "--title or --fill required when not running interactively", err.Error())
|
||||
assert.EqualError(t, err, "--title or --fill required when not running interactively")
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
}
|
||||
|
|
@ -165,7 +155,7 @@ func TestPRCreate_recover(t *testing.T) {
|
|||
"clientMutationId": ""
|
||||
} } }
|
||||
`, func(inputs map[string]interface{}) {
|
||||
eq(t, inputs["userIds"], []interface{}{"JILLID"})
|
||||
assert.Equal(t, []interface{}{"JILLID"}, inputs["userIds"])
|
||||
}))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
||||
|
|
@ -178,6 +168,7 @@ func TestPRCreate_recover(t *testing.T) {
|
|||
assert.Equal(t, "recovered body", input["body"].(string))
|
||||
}))
|
||||
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
|
||||
|
|
@ -225,7 +216,7 @@ func TestPRCreate_recover(t *testing.T) {
|
|||
output, err := runCommandWithRootDirOverridden(http, nil, "feature", true, args, "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
eq(t, output.String(), "https://github.com/OWNER/REPO/pull/12\n")
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String())
|
||||
}
|
||||
|
||||
func TestPRCreate_nontty(t *testing.T) {
|
||||
|
|
@ -254,6 +245,7 @@ func TestPRCreate_nontty(t *testing.T) {
|
|||
}),
|
||||
)
|
||||
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
|
||||
|
|
@ -296,6 +288,7 @@ func TestPRCreate(t *testing.T) {
|
|||
assert.Equal(t, "feature", input["headRefName"].(string))
|
||||
}))
|
||||
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
|
||||
|
|
@ -316,6 +309,58 @@ func TestPRCreate(t *testing.T) {
|
|||
assert.Equal(t, "\nCreating pull request for feature into master in OWNER/REPO\n\n", output.Stderr())
|
||||
}
|
||||
|
||||
func TestPRCreate_NoMaintainerModify(t *testing.T) {
|
||||
// TODO update this copypasta
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
|
||||
http.StubRepoInfoResponse("OWNER", "REPO", "master")
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes" : [
|
||||
] } } } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
||||
httpmock.GraphQLMutation(`
|
||||
{ "data": { "createPullRequest": { "pullRequest": {
|
||||
"URL": "https://github.com/OWNER/REPO/pull/12"
|
||||
} } } }
|
||||
`, func(input map[string]interface{}) {
|
||||
assert.Equal(t, false, input["maintainerCanModify"].(bool))
|
||||
assert.Equal(t, "REPOID", input["repositoryId"].(string))
|
||||
assert.Equal(t, "my title", input["title"].(string))
|
||||
assert.Equal(t, "my body", input["body"].(string))
|
||||
assert.Equal(t, "master", input["baseRefName"].(string))
|
||||
assert.Equal(t, "feature", input["headRefName"].(string))
|
||||
}))
|
||||
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
|
||||
cs.Stub("") // git config --get-regexp (determineTrackingBranch)
|
||||
cs.Stub("") // git show-ref --verify (determineTrackingBranch)
|
||||
cs.Stub("") // git status
|
||||
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
|
||||
cs.Stub("") // git push
|
||||
|
||||
ask, cleanupAsk := prompt.InitAskStubber()
|
||||
defer cleanupAsk()
|
||||
ask.StubOne(0)
|
||||
|
||||
output, err := runCommand(http, nil, "feature", true, `-t "my title" -b "my body" --no-maintainer-edit`)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String())
|
||||
assert.Equal(t, "\nCreating pull request for feature into master in OWNER/REPO\n\n", output.Stderr())
|
||||
}
|
||||
|
||||
func TestPRCreate_createFork(t *testing.T) {
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
|
|
@ -351,6 +396,7 @@ func TestPRCreate_createFork(t *testing.T) {
|
|||
assert.Equal(t, "monalisa:feature", input["headRefName"].(string))
|
||||
}))
|
||||
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
|
||||
|
|
@ -502,6 +548,7 @@ func TestPRCreate_nonLegacyTemplate(t *testing.T) {
|
|||
assert.Equal(t, "- commit 1\n- commit 0\n\nFixes a bug and Closes an issue", input["body"].(string))
|
||||
}))
|
||||
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
|
||||
|
|
@ -532,7 +579,7 @@ func TestPRCreate_nonLegacyTemplate(t *testing.T) {
|
|||
output, err := runCommandWithRootDirOverridden(http, nil, "feature", true, `-t "my title" -H feature`, "./fixtures/repoWithNonLegacyPRTemplates")
|
||||
require.NoError(t, err)
|
||||
|
||||
eq(t, output.String(), "https://github.com/OWNER/REPO/pull/12\n")
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String())
|
||||
}
|
||||
|
||||
func TestPRCreate_metadata(t *testing.T) {
|
||||
|
|
@ -600,8 +647,8 @@ func TestPRCreate_metadata(t *testing.T) {
|
|||
"URL": "https://github.com/OWNER/REPO/pull/12"
|
||||
} } } }
|
||||
`, func(inputs map[string]interface{}) {
|
||||
eq(t, inputs["title"], "TITLE")
|
||||
eq(t, inputs["body"], "BODY")
|
||||
assert.Equal(t, "TITLE", inputs["title"])
|
||||
assert.Equal(t, "BODY", inputs["body"])
|
||||
if v, ok := inputs["assigneeIds"]; ok {
|
||||
t.Errorf("did not expect assigneeIds: %v", v)
|
||||
}
|
||||
|
|
@ -616,11 +663,11 @@ func TestPRCreate_metadata(t *testing.T) {
|
|||
"clientMutationId": ""
|
||||
} } }
|
||||
`, func(inputs map[string]interface{}) {
|
||||
eq(t, inputs["pullRequestId"], "NEWPULLID")
|
||||
eq(t, inputs["assigneeIds"], []interface{}{"MONAID"})
|
||||
eq(t, inputs["labelIds"], []interface{}{"BUGID", "TODOID"})
|
||||
eq(t, inputs["projectIds"], []interface{}{"ROADMAPID"})
|
||||
eq(t, inputs["milestoneId"], "BIGONEID")
|
||||
assert.Equal(t, "NEWPULLID", inputs["pullRequestId"])
|
||||
assert.Equal(t, []interface{}{"MONAID"}, inputs["assigneeIds"])
|
||||
assert.Equal(t, []interface{}{"BUGID", "TODOID"}, inputs["labelIds"])
|
||||
assert.Equal(t, []interface{}{"ROADMAPID"}, inputs["projectIds"])
|
||||
assert.Equal(t, "BIGONEID", inputs["milestoneId"])
|
||||
}))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestCreateRequestReviews\b`),
|
||||
|
|
@ -629,12 +676,13 @@ func TestPRCreate_metadata(t *testing.T) {
|
|||
"clientMutationId": ""
|
||||
} } }
|
||||
`, func(inputs map[string]interface{}) {
|
||||
eq(t, inputs["pullRequestId"], "NEWPULLID")
|
||||
eq(t, inputs["userIds"], []interface{}{"HUBOTID", "MONAID"})
|
||||
eq(t, inputs["teamIds"], []interface{}{"COREID", "ROBOTID"})
|
||||
eq(t, inputs["union"], true)
|
||||
assert.Equal(t, "NEWPULLID", inputs["pullRequestId"])
|
||||
assert.Equal(t, []interface{}{"HUBOTID", "MONAID"}, inputs["userIds"])
|
||||
assert.Equal(t, []interface{}{"COREID", "ROBOTID"}, inputs["teamIds"])
|
||||
assert.Equal(t, true, inputs["union"])
|
||||
}))
|
||||
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
|
||||
|
|
@ -642,9 +690,9 @@ func TestPRCreate_metadata(t *testing.T) {
|
|||
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
|
||||
|
||||
output, err := runCommand(http, nil, "feature", true, `-t TITLE -b BODY -H feature -a monalisa -l bug -l todo -p roadmap -m 'big one.oh' -r hubot -r monalisa -r /core -r /robots`)
|
||||
eq(t, err, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
eq(t, output.String(), "https://github.com/OWNER/REPO/pull/12\n")
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String())
|
||||
}
|
||||
|
||||
func TestPRCreate_alreadyExists(t *testing.T) {
|
||||
|
|
@ -662,6 +710,7 @@ func TestPRCreate_alreadyExists(t *testing.T) {
|
|||
] } } } }`),
|
||||
)
|
||||
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
|
||||
|
|
@ -688,6 +737,7 @@ func TestPRCreate_web(t *testing.T) {
|
|||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`))
|
||||
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
|
||||
|
|
@ -705,16 +755,75 @@ func TestPRCreate_web(t *testing.T) {
|
|||
output, err := runCommand(http, nil, "feature", true, `--web`)
|
||||
require.NoError(t, err)
|
||||
|
||||
eq(t, output.String(), "")
|
||||
eq(t, output.Stderr(), "Opening github.com/OWNER/REPO/compare/master...feature in your browser.\n")
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "Opening github.com/OWNER/REPO/compare/master...feature in your browser.\n", output.Stderr())
|
||||
|
||||
eq(t, len(cs.Calls), 6)
|
||||
eq(t, strings.Join(cs.Calls[4].Args, " "), "git push --set-upstream origin HEAD:feature")
|
||||
assert.Equal(t, 6, len(cs.Calls))
|
||||
assert.Equal(t, "git push --set-upstream origin HEAD:feature", strings.Join(cs.Calls[4].Args, " "))
|
||||
browserCall := cs.Calls[5].Args
|
||||
eq(t, browserCall[len(browserCall)-1], "https://github.com/OWNER/REPO/compare/master...feature?expand=1")
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/compare/master...feature?expand=1", browserCall[len(browserCall)-1])
|
||||
}
|
||||
|
||||
func TestPRCreate_webProject(t *testing.T) {
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
|
||||
http.StubRepoInfoResponse("OWNER", "REPO", "master")
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query RepositoryProjectList\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "projects": {
|
||||
"nodes": [
|
||||
{ "name": "Cleanup", "id": "CLEANUPID", "resourcePath": "/OWNER/REPO/projects/1" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query OrganizationProjectList\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "organization": { "projects": {
|
||||
"nodes": [
|
||||
{ "name": "Triage", "id": "TRIAGEID", "resourcePath": "/orgs/ORG/projects/1" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
|
||||
cs.Stub("") // git config --get-regexp (determineTrackingBranch)
|
||||
cs.Stub("") // git show-ref --verify (determineTrackingBranch)
|
||||
cs.Stub("") // git status
|
||||
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
|
||||
cs.Stub("") // git push
|
||||
cs.Stub("") // browser
|
||||
|
||||
ask, cleanupAsk := prompt.InitAskStubber()
|
||||
defer cleanupAsk()
|
||||
ask.StubOne(0)
|
||||
|
||||
output, err := runCommand(http, nil, "feature", true, `--web -p Triage`)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "Opening github.com/OWNER/REPO/compare/master...feature in your browser.\n", output.Stderr())
|
||||
|
||||
assert.Equal(t, 6, len(cs.Calls))
|
||||
assert.Equal(t, "git push --set-upstream origin HEAD:feature", strings.Join(cs.Calls[4].Args, " "))
|
||||
browserCall := cs.Calls[5].Args
|
||||
url := strings.ReplaceAll(browserCall[len(browserCall)-1], "^", "")
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/compare/master...feature?expand=1&projects=ORG%2F1", url)
|
||||
}
|
||||
|
||||
func Test_determineTrackingBranch_empty(t *testing.T) {
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
|
||||
|
|
@ -730,6 +839,7 @@ func Test_determineTrackingBranch_empty(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_determineTrackingBranch_noMatch(t *testing.T) {
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
|
||||
|
|
@ -755,6 +865,7 @@ deadb00f refs/remotes/origin/feature`) // git show-ref --verify (ShowRefs)
|
|||
}
|
||||
|
||||
func Test_determineTrackingBranch_hasMatch(t *testing.T) {
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
|
||||
|
|
@ -779,13 +890,14 @@ deadbeef refs/remotes/upstream/feature`) // git show-ref --verify (ShowRefs)
|
|||
t.Fatal("expected result, got nil")
|
||||
}
|
||||
|
||||
eq(t, cs.Calls[1].Args, []string{"git", "show-ref", "--verify", "--", "HEAD", "refs/remotes/origin/feature", "refs/remotes/upstream/feature"})
|
||||
assert.Equal(t, []string{"git", "show-ref", "--verify", "--", "HEAD", "refs/remotes/origin/feature", "refs/remotes/upstream/feature"}, cs.Calls[1].Args)
|
||||
|
||||
eq(t, ref.RemoteName, "upstream")
|
||||
eq(t, ref.BranchName, "feature")
|
||||
assert.Equal(t, "upstream", ref.RemoteName)
|
||||
assert.Equal(t, "feature", ref.BranchName)
|
||||
}
|
||||
|
||||
func Test_determineTrackingBranch_respectTrackingConfig(t *testing.T) {
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
|
||||
|
|
@ -806,7 +918,7 @@ deadb00f refs/remotes/origin/feature`) // git show-ref --verify (ShowRefs)
|
|||
t.Errorf("expected nil result, got %v", ref)
|
||||
}
|
||||
|
||||
eq(t, cs.Calls[1].Args, []string{"git", "show-ref", "--verify", "--", "HEAD", "refs/remotes/origin/great-feat", "refs/remotes/origin/feature"})
|
||||
assert.Equal(t, []string{"git", "show-ref", "--verify", "--", "HEAD", "refs/remotes/origin/great-feat", "refs/remotes/origin/feature"}, cs.Calls[1].Args)
|
||||
}
|
||||
|
||||
func Test_generateCompareURL(t *testing.T) {
|
||||
|
|
@ -820,7 +932,7 @@ func Test_generateCompareURL(t *testing.T) {
|
|||
{
|
||||
name: "basic",
|
||||
ctx: CreateContext{
|
||||
BaseRepo: ghrepo.New("OWNER", "REPO"),
|
||||
BaseRepo: api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"),
|
||||
BaseBranch: "main",
|
||||
HeadBranchLabel: "feature",
|
||||
},
|
||||
|
|
@ -830,7 +942,7 @@ func Test_generateCompareURL(t *testing.T) {
|
|||
{
|
||||
name: "with labels",
|
||||
ctx: CreateContext{
|
||||
BaseRepo: ghrepo.New("OWNER", "REPO"),
|
||||
BaseRepo: api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"),
|
||||
BaseBranch: "a",
|
||||
HeadBranchLabel: "b",
|
||||
},
|
||||
|
|
@ -843,7 +955,7 @@ func Test_generateCompareURL(t *testing.T) {
|
|||
{
|
||||
name: "complex branch names",
|
||||
ctx: CreateContext{
|
||||
BaseRepo: ghrepo.New("OWNER", "REPO"),
|
||||
BaseRepo: api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"),
|
||||
BaseBranch: "main/trunk",
|
||||
HeadBranchLabel: "owner:feature",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -174,10 +174,7 @@ func TestPRDiff_no_current_pr(t *testing.T) {
|
|||
)
|
||||
|
||||
_, err := runCommand(http, nil, false, "")
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
assert.Equal(t, `no pull requests found for branch "feature"`, err.Error())
|
||||
assert.EqualError(t, err, `no pull requests found for branch "feature"`)
|
||||
}
|
||||
|
||||
func TestPRDiff_argument_not_found(t *testing.T) {
|
||||
|
|
@ -197,10 +194,7 @@ func TestPRDiff_argument_not_found(t *testing.T) {
|
|||
)
|
||||
|
||||
_, err := runCommand(http, nil, false, "123")
|
||||
if err == nil {
|
||||
t.Fatal("expected error", err)
|
||||
}
|
||||
assert.Equal(t, `could not find pull request diff: pull request not found`, err.Error())
|
||||
assert.EqualError(t, err, `could not find pull request diff: pull request not found`)
|
||||
}
|
||||
|
||||
func TestPRDiff_notty(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import (
|
|||
"io/ioutil"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
|
|
@ -20,12 +19,6 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func eq(t *testing.T, got interface{}, expected interface{}) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(got, expected) {
|
||||
t.Errorf("expected: %v, got: %v", expected, got)
|
||||
}
|
||||
}
|
||||
func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, error) {
|
||||
io, _, stdout, stderr := iostreams.Test()
|
||||
io.SetStdoutTTY(isTTY)
|
||||
|
|
@ -122,11 +115,11 @@ func TestPRList_filtering(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
eq(t, output.Stderr(), "")
|
||||
eq(t, output.String(), `
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
assert.Equal(t, `
|
||||
No pull requests match your search in OWNER/REPO
|
||||
|
||||
`)
|
||||
`, output.String())
|
||||
}
|
||||
|
||||
func TestPRList_filteringRemoveDuplicate(t *testing.T) {
|
||||
|
|
@ -207,6 +200,7 @@ func TestPRList_web(t *testing.T) {
|
|||
defer http.Verify(t)
|
||||
|
||||
var seenCmd *exec.Cmd
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmd = cmd
|
||||
return &test.OutputStub{}
|
||||
|
|
@ -220,12 +214,12 @@ func TestPRList_web(t *testing.T) {
|
|||
|
||||
expectedURL := "https://github.com/OWNER/REPO/pulls?q=is%3Apr+is%3Amerged+assignee%3Apeter+label%3Abug+label%3Adocs+base%3Atrunk"
|
||||
|
||||
eq(t, output.String(), "")
|
||||
eq(t, output.Stderr(), "Opening github.com/OWNER/REPO/pulls in your browser.\n")
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "Opening github.com/OWNER/REPO/pulls in your browser.\n", output.Stderr())
|
||||
|
||||
if seenCmd == nil {
|
||||
t.Fatal("expected a command to run")
|
||||
}
|
||||
url := seenCmd.Args[len(seenCmd.Args)-1]
|
||||
eq(t, url, expectedURL)
|
||||
assert.Equal(t, url, expectedURL)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ type MergeOptions struct {
|
|||
DeleteBranch bool
|
||||
MergeMethod api.PullRequestMergeMethod
|
||||
|
||||
Body *string
|
||||
|
||||
IsDeleteBranchIndicated bool
|
||||
CanDeleteLocalBranch bool
|
||||
InteractiveMode bool
|
||||
|
|
@ -95,6 +97,11 @@ func NewCmdMerge(f *cmdutil.Factory, runF func(*MergeOptions) error) *cobra.Comm
|
|||
opts.IsDeleteBranchIndicated = cmd.Flags().Changed("delete-branch")
|
||||
opts.CanDeleteLocalBranch = !cmd.Flags().Changed("repo")
|
||||
|
||||
if cmd.Flags().Changed("body") {
|
||||
bodyStr, _ := cmd.Flags().GetString("body")
|
||||
opts.Body = &bodyStr
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
|
|
@ -103,6 +110,7 @@ func NewCmdMerge(f *cmdutil.Factory, runF func(*MergeOptions) error) *cobra.Comm
|
|||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&opts.DeleteBranch, "delete-branch", "d", false, "Delete the local and remote branch after merge")
|
||||
cmd.Flags().StringP("body", "b", "", "Body for merge commit")
|
||||
cmd.Flags().BoolVarP(&flagMerge, "merge", "m", false, "Merge the commits with the base branch")
|
||||
cmd.Flags().BoolVarP(&flagRebase, "rebase", "r", false, "Rebase the commits onto the base branch")
|
||||
cmd.Flags().BoolVarP(&flagSquash, "squash", "s", false, "Squash the commits into one commit and merge it into the base branch")
|
||||
|
|
@ -124,8 +132,8 @@ func mergeRun(opts *MergeOptions) error {
|
|||
}
|
||||
|
||||
if pr.Mergeable == "CONFLICTING" {
|
||||
err := fmt.Errorf("%s Pull request #%d (%s) has conflicts and isn't mergeable ", cs.Red("!"), pr.Number, pr.Title)
|
||||
return err
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Pull request #%d (%s) has conflicts and isn't mergeable\n", cs.Red("!"), pr.Number, pr.Title)
|
||||
return cmdutil.SilentError
|
||||
}
|
||||
|
||||
deleteBranch := opts.DeleteBranch
|
||||
|
|
@ -137,17 +145,29 @@ func mergeRun(opts *MergeOptions) error {
|
|||
mergeMethod := opts.MergeMethod
|
||||
|
||||
if opts.InteractiveMode {
|
||||
mergeMethod, deleteBranch, err = prInteractiveMerge(opts, crossRepoPR)
|
||||
r, err := api.GitHubRepo(apiClient, baseRepo)
|
||||
if err != nil {
|
||||
if errors.Is(err, cancelError) {
|
||||
fmt.Fprintln(opts.IO.ErrOut, "Cancelled.")
|
||||
return cmdutil.SilentError
|
||||
}
|
||||
return err
|
||||
}
|
||||
mergeMethod, err = mergeMethodSurvey(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deleteBranch, err = deleteBranchSurvey(opts, crossRepoPR)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
confirm, err := confirmSurvey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !confirm {
|
||||
fmt.Fprintln(opts.IO.ErrOut, "Cancelled.")
|
||||
return cmdutil.SilentError
|
||||
}
|
||||
}
|
||||
|
||||
err = api.PullRequestMerge(apiClient, baseRepo, pr, mergeMethod)
|
||||
err = api.PullRequestMerge(apiClient, baseRepo, pr, mergeMethod, opts.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -162,7 +182,7 @@ func mergeRun(opts *MergeOptions) error {
|
|||
}
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s %s pull request #%d (%s)\n", cs.SuccessIconOfColor(cs.Magenta), action, pr.Number, pr.Title)
|
||||
}
|
||||
} else if !opts.IsDeleteBranchIndicated && opts.InteractiveMode {
|
||||
} else if !opts.IsDeleteBranchIndicated && opts.InteractiveMode && !crossRepoPR {
|
||||
err := prompt.SurveyAskOne(&survey.Confirm{
|
||||
Message: fmt.Sprintf("Pull request #%d was already merged. Delete the branch locally?", pr.Number),
|
||||
Default: false,
|
||||
|
|
@ -170,15 +190,17 @@ func mergeRun(opts *MergeOptions) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
} else if crossRepoPR {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Pull request #%d was already merged\n", cs.WarningIcon(), pr.Number)
|
||||
}
|
||||
|
||||
if !deleteBranch {
|
||||
if !deleteBranch || crossRepoPR {
|
||||
return nil
|
||||
}
|
||||
|
||||
branchSwitchString := ""
|
||||
|
||||
if opts.CanDeleteLocalBranch && !crossRepoPR {
|
||||
if opts.CanDeleteLocalBranch {
|
||||
currentBranch, err := opts.Branch()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -210,7 +232,7 @@ func mergeRun(opts *MergeOptions) error {
|
|||
}
|
||||
}
|
||||
|
||||
if !isPRAlreadyMerged && !crossRepoPR {
|
||||
if !isPRAlreadyMerged {
|
||||
err = api.BranchDeleteRemote(apiClient, baseRepo, pr.HeadRefName)
|
||||
var httpErr api.HTTPError
|
||||
// The ref might have already been deleted by GitHub
|
||||
|
|
@ -227,20 +249,43 @@ func mergeRun(opts *MergeOptions) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
var cancelError = errors.New("cancelError")
|
||||
|
||||
func prInteractiveMerge(opts *MergeOptions, crossRepoPR bool) (api.PullRequestMergeMethod, bool, error) {
|
||||
mergeMethodQuestion := &survey.Question{
|
||||
Name: "mergeMethod",
|
||||
Prompt: &survey.Select{
|
||||
Message: "What merge method would you like to use?",
|
||||
Options: []string{"Create a merge commit", "Rebase and merge", "Squash and merge"},
|
||||
Default: "Create a merge commit",
|
||||
},
|
||||
func mergeMethodSurvey(baseRepo *api.Repository) (api.PullRequestMergeMethod, error) {
|
||||
type mergeOption struct {
|
||||
title string
|
||||
method api.PullRequestMergeMethod
|
||||
}
|
||||
|
||||
qs := []*survey.Question{mergeMethodQuestion}
|
||||
var mergeOpts []mergeOption
|
||||
if baseRepo.MergeCommitAllowed {
|
||||
opt := mergeOption{title: "Create a merge commit", method: api.PullRequestMergeMethodMerge}
|
||||
mergeOpts = append(mergeOpts, opt)
|
||||
}
|
||||
if baseRepo.RebaseMergeAllowed {
|
||||
opt := mergeOption{title: "Rebase and merge", method: api.PullRequestMergeMethodRebase}
|
||||
mergeOpts = append(mergeOpts, opt)
|
||||
}
|
||||
if baseRepo.SquashMergeAllowed {
|
||||
opt := mergeOption{title: "Squash and merge", method: api.PullRequestMergeMethodSquash}
|
||||
mergeOpts = append(mergeOpts, opt)
|
||||
}
|
||||
|
||||
var surveyOpts []string
|
||||
for _, v := range mergeOpts {
|
||||
surveyOpts = append(surveyOpts, v.title)
|
||||
}
|
||||
|
||||
mergeQuestion := &survey.Select{
|
||||
Message: "What merge method would you like to use?",
|
||||
Options: surveyOpts,
|
||||
Default: "Create a merge commit",
|
||||
}
|
||||
|
||||
var result int
|
||||
err := prompt.SurveyAskOne(mergeQuestion, &result)
|
||||
return mergeOpts[result].method, err
|
||||
}
|
||||
|
||||
func deleteBranchSurvey(opts *MergeOptions, crossRepoPR bool) (bool, error) {
|
||||
if !crossRepoPR && !opts.IsDeleteBranchIndicated {
|
||||
var message string
|
||||
if opts.CanDeleteLocalBranch {
|
||||
|
|
@ -249,49 +294,24 @@ func prInteractiveMerge(opts *MergeOptions, crossRepoPR bool) (api.PullRequestMe
|
|||
message = "Delete the branch on GitHub?"
|
||||
}
|
||||
|
||||
deleteBranchQuestion := &survey.Question{
|
||||
Name: "deleteBranch",
|
||||
Prompt: &survey.Confirm{
|
||||
Message: message,
|
||||
Default: false,
|
||||
},
|
||||
}
|
||||
qs = append(qs, deleteBranchQuestion)
|
||||
}
|
||||
|
||||
qs = append(qs, &survey.Question{
|
||||
Name: "isConfirmed",
|
||||
Prompt: &survey.Confirm{
|
||||
Message: "Submit?",
|
||||
var result bool
|
||||
submit := &survey.Confirm{
|
||||
Message: message,
|
||||
Default: false,
|
||||
},
|
||||
})
|
||||
|
||||
answers := struct {
|
||||
MergeMethod int
|
||||
DeleteBranch bool
|
||||
IsConfirmed bool
|
||||
}{
|
||||
DeleteBranch: opts.DeleteBranch,
|
||||
}
|
||||
err := prompt.SurveyAskOne(submit, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
err := prompt.SurveyAsk(qs, &answers)
|
||||
if err != nil {
|
||||
return 0, false, fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
if !answers.IsConfirmed {
|
||||
return 0, false, cancelError
|
||||
}
|
||||
|
||||
var mergeMethod api.PullRequestMergeMethod
|
||||
switch answers.MergeMethod {
|
||||
case 0:
|
||||
mergeMethod = api.PullRequestMergeMethodMerge
|
||||
case 1:
|
||||
mergeMethod = api.PullRequestMergeMethodRebase
|
||||
case 2:
|
||||
mergeMethod = api.PullRequestMergeMethodSquash
|
||||
}
|
||||
|
||||
return mergeMethod, answers.DeleteBranch, nil
|
||||
return opts.DeleteBranch, nil
|
||||
}
|
||||
|
||||
func confirmSurvey() (bool, error) {
|
||||
var confirm bool
|
||||
submit := &survey.Confirm{
|
||||
Message: "Submit?",
|
||||
Default: true,
|
||||
}
|
||||
err := prompt.SurveyAskOne(submit, &confirm)
|
||||
return confirm, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,11 +27,12 @@ import (
|
|||
|
||||
func Test_NewCmdMerge(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args string
|
||||
isTTY bool
|
||||
want MergeOptions
|
||||
wantErr string
|
||||
name string
|
||||
args string
|
||||
isTTY bool
|
||||
want MergeOptions
|
||||
wantBody string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "number argument",
|
||||
|
|
@ -59,6 +60,20 @@ func Test_NewCmdMerge(t *testing.T) {
|
|||
InteractiveMode: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "body",
|
||||
args: "123 -bcool",
|
||||
isTTY: true,
|
||||
want: MergeOptions{
|
||||
SelectorArg: "123",
|
||||
DeleteBranch: false,
|
||||
IsDeleteBranchIndicated: false,
|
||||
CanDeleteLocalBranch: true,
|
||||
MergeMethod: api.PullRequestMergeMethodMerge,
|
||||
InteractiveMode: true,
|
||||
},
|
||||
wantBody: "cool",
|
||||
},
|
||||
{
|
||||
name: "no argument with --repo override",
|
||||
args: "-R owner/repo",
|
||||
|
|
@ -123,6 +138,12 @@ func Test_NewCmdMerge(t *testing.T) {
|
|||
assert.Equal(t, tt.want.CanDeleteLocalBranch, opts.CanDeleteLocalBranch)
|
||||
assert.Equal(t, tt.want.MergeMethod, opts.MergeMethod)
|
||||
assert.Equal(t, tt.want.InteractiveMode, opts.InteractiveMode)
|
||||
|
||||
if tt.wantBody == "" {
|
||||
assert.Nil(t, opts.Body)
|
||||
} else {
|
||||
assert.Equal(t, tt.wantBody, *opts.Body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -208,14 +229,8 @@ func TestPrMerge(t *testing.T) {
|
|||
assert.NotContains(t, input, "commitHeadline")
|
||||
}))
|
||||
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
|
||||
cs.Stub("branch.blueberries.remote origin\nbranch.blueberries.merge refs/heads/blueberries") // git config --get-regexp ^branch\.master\.(remote|merge)
|
||||
cs.Stub("") // git config --get-regexp ^branch\.blueberries\.(remote|merge)$
|
||||
cs.Stub("") // git symbolic-ref --quiet --short HEAD
|
||||
cs.Stub("") // git checkout master
|
||||
cs.Stub("")
|
||||
_, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
output, err := runCommand(http, "master", true, "pr merge 1 --merge")
|
||||
if err != nil {
|
||||
|
|
@ -251,14 +266,8 @@ func TestPrMerge_nontty(t *testing.T) {
|
|||
assert.NotContains(t, input, "commitHeadline")
|
||||
}))
|
||||
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
|
||||
cs.Stub("branch.blueberries.remote origin\nbranch.blueberries.merge refs/heads/blueberries") // git config --get-regexp ^branch\.master\.(remote|merge)
|
||||
cs.Stub("") // git config --get-regexp ^branch\.blueberries\.(remote|merge)$
|
||||
cs.Stub("") // git symbolic-ref --quiet --short HEAD
|
||||
cs.Stub("") // git checkout master
|
||||
cs.Stub("")
|
||||
_, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
output, err := runCommand(http, "master", false, "pr merge 1 --merge")
|
||||
if err != nil {
|
||||
|
|
@ -291,16 +300,14 @@ func TestPrMerge_withRepoFlag(t *testing.T) {
|
|||
assert.NotContains(t, input, "commitHeadline")
|
||||
}))
|
||||
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
_, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
output, err := runCommand(http, "master", true, "pr merge 1 --merge -R OWNER/REPO")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `pr merge`: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 0, len(cs.Calls))
|
||||
|
||||
r := regexp.MustCompile(`Merged pull request #1 \(The title of the PR\)`)
|
||||
|
||||
if !r.MatchString(output.Stderr()) {
|
||||
|
|
@ -326,20 +333,20 @@ func TestPrMerge_deleteBranch(t *testing.T) {
|
|||
httpmock.REST("DELETE", "repos/OWNER/REPO/git/refs/heads/blueberries"),
|
||||
httpmock.StringResponse(`{}`))
|
||||
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
cs.Stub("") // git config --get-regexp ^branch\.blueberries\.(remote|merge)$
|
||||
cs.Stub("") // git checkout master
|
||||
cs.Stub("") // git rev-parse --verify blueberries`
|
||||
cs.Stub("") // git branch -d
|
||||
cs.Stub("") // git push origin --delete blueberries
|
||||
cs.Register(`git config --get-regexp.+branch\\\.blueberries\\\.`, 0, "")
|
||||
cs.Register(`git checkout master`, 0, "")
|
||||
cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "")
|
||||
cs.Register(`git branch -D blueberries`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, `pr merge --merge --delete-branch`)
|
||||
if err != nil {
|
||||
t.Fatalf("Got unexpected error running `pr merge` %s", err)
|
||||
}
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), `Merged pull request #10 \(Blueberries are a good fruit\)`, `Deleted branch.*blueberries`)
|
||||
}
|
||||
|
||||
|
|
@ -361,19 +368,18 @@ func TestPrMerge_deleteNonCurrentBranch(t *testing.T) {
|
|||
httpmock.REST("DELETE", "repos/OWNER/REPO/git/refs/heads/blueberries"),
|
||||
httpmock.StringResponse(`{}`))
|
||||
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
// We don't expect the default branch to be checked out, just that blueberries is deleted
|
||||
cs.Stub("") // git rev-parse --verify blueberries
|
||||
cs.Stub("") // git branch -d blueberries
|
||||
cs.Stub("") // git push origin --delete blueberries
|
||||
cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "")
|
||||
cs.Register(`git branch -D blueberries`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "master", true, `pr merge --merge --delete-branch blueberries`)
|
||||
if err != nil {
|
||||
t.Fatalf("Got unexpected error running `pr merge` %s", err)
|
||||
}
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), `Merged pull request #10 \(Blueberries are a good fruit\)`, `Deleted branch.*blueberries`)
|
||||
}
|
||||
|
||||
|
|
@ -392,14 +398,10 @@ func TestPrMerge_noPrNumberGiven(t *testing.T) {
|
|||
assert.NotContains(t, input, "commitHeadline")
|
||||
}))
|
||||
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
cs.Stub("branch.blueberries.remote origin\nbranch.blueberries.merge refs/heads/blueberries") // git config --get-regexp ^branch\.master\.(remote|merge)
|
||||
cs.Stub("") // git config --get-regexp ^branch\.blueberries\.(remote|merge)$
|
||||
cs.Stub("") // git symbolic-ref --quiet --short HEAD
|
||||
cs.Stub("") // git checkout master
|
||||
cs.Stub("") // git branch -d
|
||||
cs.Register(`git config --get-regexp.+branch\\\.blueberries\\\.`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "pr merge --merge")
|
||||
if err != nil {
|
||||
|
|
@ -435,13 +437,8 @@ func TestPrMerge_rebase(t *testing.T) {
|
|||
assert.NotContains(t, input, "commitHeadline")
|
||||
}))
|
||||
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
|
||||
cs.Stub("") // git config --get-regexp ^branch\.blueberries\.(remote|merge)$
|
||||
cs.Stub("") // git symbolic-ref --quiet --short HEAD
|
||||
cs.Stub("") // git checkout master
|
||||
cs.Stub("") // git branch -d
|
||||
_, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
output, err := runCommand(http, "master", true, "pr merge 2 --rebase")
|
||||
if err != nil {
|
||||
|
|
@ -477,19 +474,15 @@ func TestPrMerge_squash(t *testing.T) {
|
|||
assert.Equal(t, "The title of the PR (#3)", input["commitHeadline"].(string))
|
||||
}))
|
||||
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
|
||||
cs.Stub("") // git config --get-regexp ^branch\.blueberries\.(remote|merge)$
|
||||
cs.Stub("") // git symbolic-ref --quiet --short HEAD
|
||||
cs.Stub("") // git checkout master
|
||||
cs.Stub("") // git branch -d
|
||||
_, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
output, err := runCommand(http, "master", true, "pr merge 3 --squash")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `pr merge`: %v", err)
|
||||
}
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), "Squashed and merged pull request #3")
|
||||
}
|
||||
|
||||
|
|
@ -529,6 +522,7 @@ func TestPrMerge_alreadyMerged(t *testing.T) {
|
|||
t.Fatalf("Got unexpected error running `pr merge` %s", err)
|
||||
}
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), "✓ Deleted branch blueberries and switched to branch master")
|
||||
}
|
||||
|
||||
|
|
@ -551,7 +545,7 @@ func TestPrMerge_alreadyMerged_nonInteractive(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
assert.Equal(t, "! Pull request #4 was already merged\n", output.Stderr())
|
||||
}
|
||||
|
||||
func TestPRMerge_interactive(t *testing.T) {
|
||||
|
|
@ -566,6 +560,14 @@ func TestPRMerge_interactive(t *testing.T) {
|
|||
"id": "THE-ID",
|
||||
"number": 3
|
||||
}] } } } }`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query RepositoryInfo\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"mergeCommitAllowed": true,
|
||||
"rebaseMergeAllowed": true,
|
||||
"squashMergeAllowed": true
|
||||
} } }`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestMerge\b`),
|
||||
httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) {
|
||||
|
|
@ -577,38 +579,27 @@ func TestPRMerge_interactive(t *testing.T) {
|
|||
httpmock.REST("DELETE", "repos/OWNER/REPO/git/refs/heads/blueberries"),
|
||||
httpmock.StringResponse(`{}`))
|
||||
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
cs.Stub("") // git config --get-regexp ^branch\.blueberries\.(remote|merge)$
|
||||
cs.Stub("") // git symbolic-ref --quiet --short HEAD
|
||||
cs.Stub("") // git checkout master
|
||||
cs.Stub("") // git push origin --delete blueberries
|
||||
cs.Stub("") // git branch -d
|
||||
cs.Register(`git config --get-regexp.+branch\\\.blueberries\\\.`, 0, "")
|
||||
cs.Register(`git checkout master`, 0, "")
|
||||
cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "")
|
||||
cs.Register(`git branch -D blueberries`, 0, "")
|
||||
|
||||
as, surveyTeardown := prompt.InitAskStubber()
|
||||
defer surveyTeardown()
|
||||
|
||||
as.Stub([]*prompt.QuestionStub{
|
||||
{
|
||||
Name: "mergeMethod",
|
||||
Value: 0,
|
||||
},
|
||||
{
|
||||
Name: "deleteBranch",
|
||||
Value: true,
|
||||
},
|
||||
{
|
||||
Name: "isConfirmed",
|
||||
Value: true,
|
||||
},
|
||||
})
|
||||
as.StubOne(0) // Merge method survey
|
||||
as.StubOne(true) // Delete branch survey
|
||||
as.StubOne(true) // Confirm submit survey
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "")
|
||||
if err != nil {
|
||||
t.Fatalf("Got unexpected error running `pr merge` %s", err)
|
||||
}
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), "Merged pull request #3")
|
||||
}
|
||||
|
||||
|
|
@ -624,6 +615,14 @@ func TestPRMerge_interactiveWithDeleteBranch(t *testing.T) {
|
|||
"id": "THE-ID",
|
||||
"number": 3
|
||||
}] } } } }`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query RepositoryInfo\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"mergeCommitAllowed": true,
|
||||
"rebaseMergeAllowed": true,
|
||||
"squashMergeAllowed": true
|
||||
} } }`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestMerge\b`),
|
||||
httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) {
|
||||
|
|
@ -635,34 +634,26 @@ func TestPRMerge_interactiveWithDeleteBranch(t *testing.T) {
|
|||
httpmock.REST("DELETE", "repos/OWNER/REPO/git/refs/heads/blueberries"),
|
||||
httpmock.StringResponse(`{}`))
|
||||
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
cs.Stub("") // git config --get-regexp ^branch\.blueberries\.(remote|merge)$
|
||||
cs.Stub("") // git symbolic-ref --quiet --short HEAD
|
||||
cs.Stub("") // git checkout master
|
||||
cs.Stub("") // git push origin --delete blueberries
|
||||
cs.Stub("") // git branch -d
|
||||
cs.Register(`git config --get-regexp.+branch\\\.blueberries\\\.`, 0, "")
|
||||
cs.Register(`git checkout master`, 0, "")
|
||||
cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "")
|
||||
cs.Register(`git branch -D blueberries`, 0, "")
|
||||
|
||||
as, surveyTeardown := prompt.InitAskStubber()
|
||||
defer surveyTeardown()
|
||||
|
||||
as.Stub([]*prompt.QuestionStub{
|
||||
{
|
||||
Name: "mergeMethod",
|
||||
Value: 0,
|
||||
},
|
||||
{
|
||||
Name: "isConfirmed",
|
||||
Value: true,
|
||||
},
|
||||
})
|
||||
as.StubOne(0) // Merge method survey
|
||||
as.StubOne(true) // Confirm submit survey
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "-d")
|
||||
if err != nil {
|
||||
t.Fatalf("Got unexpected error running `pr merge` %s", err)
|
||||
}
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), "Merged pull request #3", "Deleted branch blueberries and switched to branch master")
|
||||
}
|
||||
|
||||
|
|
@ -678,33 +669,26 @@ func TestPRMerge_interactiveCancelled(t *testing.T) {
|
|||
"id": "THE-ID",
|
||||
"number": 3
|
||||
}] } } } }`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query RepositoryInfo\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"mergeCommitAllowed": true,
|
||||
"rebaseMergeAllowed": true,
|
||||
"squashMergeAllowed": true
|
||||
} } }`))
|
||||
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
cs.Stub("") // git config --get-regexp ^branch\.blueberries\.(remote|merge)$
|
||||
cs.Stub("") // git symbolic-ref --quiet --short HEAD
|
||||
cs.Stub("") // git checkout master
|
||||
cs.Stub("") // git push origin --delete blueberries
|
||||
cs.Stub("") // git branch -d
|
||||
cs.Register(`git config --get-regexp.+branch\\\.blueberries\\\.`, 0, "")
|
||||
|
||||
as, surveyTeardown := prompt.InitAskStubber()
|
||||
defer surveyTeardown()
|
||||
|
||||
as.Stub([]*prompt.QuestionStub{
|
||||
{
|
||||
Name: "mergeMethod",
|
||||
Value: 0,
|
||||
},
|
||||
{
|
||||
Name: "deleteBranch",
|
||||
Value: true,
|
||||
},
|
||||
{
|
||||
Name: "isConfirmed",
|
||||
Value: false,
|
||||
},
|
||||
})
|
||||
as.StubOne(0) // Merge method survey
|
||||
as.StubOne(true) // Delete branch survey
|
||||
as.StubOne(false) // Confirm submit survey
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "")
|
||||
if !errors.Is(err, cmdutil.SilentError) {
|
||||
|
|
@ -713,3 +697,17 @@ func TestPRMerge_interactiveCancelled(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "Cancelled.\n", output.Stderr())
|
||||
}
|
||||
|
||||
func Test_mergeMethodSurvey(t *testing.T) {
|
||||
repo := &api.Repository{
|
||||
MergeCommitAllowed: false,
|
||||
RebaseMergeAllowed: true,
|
||||
SquashMergeAllowed: true,
|
||||
}
|
||||
as, surveyTeardown := prompt.InitAskStubber()
|
||||
defer surveyTeardown()
|
||||
as.StubOne(0) // Select first option which is rebase merge
|
||||
method, err := mergeMethodSurvey(repo)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, api.PullRequestMergeMethodRebase, method)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
cmdCheckout "github.com/cli/cli/pkg/cmd/pr/checkout"
|
||||
cmdChecks "github.com/cli/cli/pkg/cmd/pr/checks"
|
||||
cmdClose "github.com/cli/cli/pkg/cmd/pr/close"
|
||||
cmdComment "github.com/cli/cli/pkg/cmd/pr/comment"
|
||||
cmdCreate "github.com/cli/cli/pkg/cmd/pr/create"
|
||||
cmdDiff "github.com/cli/cli/pkg/cmd/pr/diff"
|
||||
cmdList "github.com/cli/cli/pkg/cmd/pr/list"
|
||||
|
|
@ -53,6 +54,7 @@ func NewCmdPR(f *cmdutil.Factory) *cobra.Command {
|
|||
cmd.AddCommand(cmdStatus.NewCmdStatus(f, nil))
|
||||
cmd.AddCommand(cmdView.NewCmdView(f, nil))
|
||||
cmd.AddCommand(cmdChecks.NewCmdChecks(f, nil))
|
||||
cmd.AddCommand(cmdComment.NewCmdComment(f, nil))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,13 +60,13 @@ func NewCmdReview(f *cmdutil.Factory, runF func(*ReviewOptions) error) *cobra.Co
|
|||
Example: heredoc.Doc(`
|
||||
# approve the pull request of the current branch
|
||||
$ gh pr review --approve
|
||||
|
||||
|
||||
# leave a review comment for the current branch
|
||||
$ gh pr review --comment -b "interesting"
|
||||
|
||||
|
||||
# add a review for a specific pull request
|
||||
$ gh pr review 123
|
||||
|
||||
|
||||
# request changes on a specific pull request
|
||||
$ gh pr review 123 -r -b "needs more ASCII art"
|
||||
`),
|
||||
|
|
|
|||
|
|
@ -218,6 +218,7 @@ func TestPRReview_url_arg(t *testing.T) {
|
|||
t.Fatalf("error running pr review: %s", err)
|
||||
}
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), "Approved pull request #123")
|
||||
}
|
||||
|
||||
|
|
@ -260,6 +261,7 @@ func TestPRReview_number_arg(t *testing.T) {
|
|||
t.Fatalf("error running pr review: %s", err)
|
||||
}
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), "Approved pull request #123")
|
||||
}
|
||||
|
||||
|
|
@ -293,6 +295,7 @@ func TestPRReview_no_arg(t *testing.T) {
|
|||
t.Fatalf("error running pr review: %s", err)
|
||||
}
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), "Reviewed pull request #123")
|
||||
}
|
||||
|
||||
|
|
@ -425,8 +428,10 @@ func TestPRReview_interactive(t *testing.T) {
|
|||
t.Fatalf("got unexpected error running pr review: %s", err)
|
||||
}
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), "Approved pull request #123")
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.String(),
|
||||
"Got:",
|
||||
"cool.*story")
|
||||
|
|
@ -470,10 +475,7 @@ func TestPRReview_interactive_no_body(t *testing.T) {
|
|||
})
|
||||
|
||||
_, err := runCommand(http, nil, true, "")
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
assert.Equal(t, "this type of review cannot be blank", err.Error())
|
||||
assert.EqualError(t, err, "this type of review cannot be blank")
|
||||
}
|
||||
|
||||
func TestPRReview_interactive_blank_approve(t *testing.T) {
|
||||
|
|
@ -532,5 +534,6 @@ func TestPRReview_interactive_blank_approve(t *testing.T) {
|
|||
t.Errorf("did not expect to see body printed in %s", output.String())
|
||||
}
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), "Approved pull request #123")
|
||||
}
|
||||
|
|
|
|||
168
pkg/cmd/pr/shared/commentable.go
Normal file
168
pkg/cmd/pr/shared/commentable.go
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/pkg/surveyext"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type InputType int
|
||||
|
||||
const (
|
||||
InputTypeEditor InputType = iota
|
||||
InputTypeInline
|
||||
InputTypeWeb
|
||||
)
|
||||
|
||||
type Commentable interface {
|
||||
Link() string
|
||||
Identifier() string
|
||||
}
|
||||
|
||||
type CommentableOptions struct {
|
||||
IO *iostreams.IOStreams
|
||||
HttpClient func() (*http.Client, error)
|
||||
RetrieveCommentable func() (Commentable, ghrepo.Interface, error)
|
||||
EditSurvey func() (string, error)
|
||||
InteractiveEditSurvey func() (string, error)
|
||||
ConfirmSubmitSurvey func() (bool, error)
|
||||
OpenInBrowser func(string) error
|
||||
Interactive bool
|
||||
InputType InputType
|
||||
Body string
|
||||
}
|
||||
|
||||
func CommentablePreRun(cmd *cobra.Command, opts *CommentableOptions) error {
|
||||
inputFlags := 0
|
||||
if cmd.Flags().Changed("body") {
|
||||
opts.InputType = InputTypeInline
|
||||
inputFlags++
|
||||
}
|
||||
if web, _ := cmd.Flags().GetBool("web"); web {
|
||||
opts.InputType = InputTypeWeb
|
||||
inputFlags++
|
||||
}
|
||||
if editor, _ := cmd.Flags().GetBool("editor"); editor {
|
||||
opts.InputType = InputTypeEditor
|
||||
inputFlags++
|
||||
}
|
||||
|
||||
if inputFlags == 0 {
|
||||
if !opts.IO.CanPrompt() {
|
||||
return &cmdutil.FlagError{Err: errors.New("--body or --web required when not running interactively")}
|
||||
}
|
||||
opts.Interactive = true
|
||||
} else if inputFlags == 1 {
|
||||
if !opts.IO.CanPrompt() && opts.InputType == InputTypeEditor {
|
||||
return &cmdutil.FlagError{Err: errors.New("--body or --web required when not running interactively")}
|
||||
}
|
||||
} else if inputFlags > 1 {
|
||||
return &cmdutil.FlagError{Err: fmt.Errorf("specify only one of --body, --editor, or --web")}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CommentableRun(opts *CommentableOptions) error {
|
||||
commentable, repo, err := opts.RetrieveCommentable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch opts.InputType {
|
||||
case InputTypeWeb:
|
||||
openURL := commentable.Link() + "#issuecomment-new"
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
|
||||
}
|
||||
return opts.OpenInBrowser(openURL)
|
||||
case InputTypeEditor:
|
||||
var body string
|
||||
if opts.Interactive {
|
||||
body, err = opts.InteractiveEditSurvey()
|
||||
} else {
|
||||
body, err = opts.EditSurvey()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Body = body
|
||||
}
|
||||
|
||||
if opts.Interactive {
|
||||
cont, err := opts.ConfirmSubmitSurvey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !cont {
|
||||
return errors.New("Discarding...")
|
||||
}
|
||||
}
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
params := api.CommentCreateInput{Body: opts.Body, SubjectId: commentable.Identifier()}
|
||||
url, err := api.CommentCreate(apiClient, repo.RepoHost(), params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(opts.IO.Out, url)
|
||||
return nil
|
||||
}
|
||||
|
||||
func CommentableConfirmSubmitSurvey() (bool, error) {
|
||||
var confirm bool
|
||||
submit := &survey.Confirm{
|
||||
Message: "Submit?",
|
||||
Default: true,
|
||||
}
|
||||
err := survey.AskOne(submit, &confirm)
|
||||
return confirm, err
|
||||
}
|
||||
|
||||
func CommentableInteractiveEditSurvey(cf func() (config.Config, error), io *iostreams.IOStreams) func() (string, error) {
|
||||
return func() (string, error) {
|
||||
editorCommand, err := cmdutil.DetermineEditor(cf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if editorCommand == "" {
|
||||
editorCommand = surveyext.DefaultEditorName()
|
||||
}
|
||||
cs := io.ColorScheme()
|
||||
fmt.Fprintf(io.Out, "- %s to draft your comment in %s... ", cs.Bold("Press Enter"), cs.Bold(editorCommand))
|
||||
_ = waitForEnter(io.In)
|
||||
return surveyext.Edit(editorCommand, "*.md", "", io.In, io.Out, io.ErrOut, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func CommentableEditSurvey(cf func() (config.Config, error), io *iostreams.IOStreams) func() (string, error) {
|
||||
return func() (string, error) {
|
||||
editorCommand, err := cmdutil.DetermineEditor(cf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return surveyext.Edit(editorCommand, "*.md", "", io.In, io.Out, io.ErrOut, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func waitForEnter(r io.Reader) error {
|
||||
scanner := bufio.NewScanner(r)
|
||||
scanner.Scan()
|
||||
return scanner.Err()
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package shared
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -11,37 +12,55 @@ import (
|
|||
"github.com/cli/cli/utils"
|
||||
)
|
||||
|
||||
func RawCommentList(comments api.Comments) string {
|
||||
type Comment interface {
|
||||
AuthorLogin() string
|
||||
Association() string
|
||||
Content() string
|
||||
Created() time.Time
|
||||
IsEdited() bool
|
||||
Link() string
|
||||
Reactions() api.ReactionGroups
|
||||
Status() string
|
||||
}
|
||||
|
||||
func RawCommentList(comments api.Comments, reviews api.PullRequestReviews) string {
|
||||
sortedComments := sortComments(comments, reviews)
|
||||
var b strings.Builder
|
||||
for _, comment := range comments.Nodes {
|
||||
for _, comment := range sortedComments {
|
||||
fmt.Fprint(&b, formatRawComment(comment))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func formatRawComment(comment api.Comment) string {
|
||||
func formatRawComment(comment Comment) string {
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "author:\t%s\n", comment.Author.Login)
|
||||
fmt.Fprintf(&b, "association:\t%s\n", strings.ToLower(comment.AuthorAssociation))
|
||||
fmt.Fprintf(&b, "edited:\t%t\n", comment.IncludesCreatedEdit)
|
||||
fmt.Fprintf(&b, "author:\t%s\n", comment.AuthorLogin())
|
||||
fmt.Fprintf(&b, "association:\t%s\n", strings.ToLower(comment.Association()))
|
||||
fmt.Fprintf(&b, "edited:\t%t\n", comment.IsEdited())
|
||||
fmt.Fprintf(&b, "status:\t%s\n", formatRawCommentStatus(comment.Status()))
|
||||
fmt.Fprintln(&b, "--")
|
||||
fmt.Fprintln(&b, comment.Body)
|
||||
fmt.Fprintln(&b, comment.Content())
|
||||
fmt.Fprintln(&b, "--")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func CommentList(io *iostreams.IOStreams, comments api.Comments) (string, error) {
|
||||
func CommentList(io *iostreams.IOStreams, comments api.Comments, reviews api.PullRequestReviews, preview bool) (string, error) {
|
||||
sortedComments := sortComments(comments, reviews)
|
||||
if preview && len(sortedComments) > 0 {
|
||||
sortedComments = sortedComments[len(sortedComments)-1:]
|
||||
}
|
||||
var b strings.Builder
|
||||
cs := io.ColorScheme()
|
||||
retrievedCount := len(comments.Nodes)
|
||||
hiddenCount := comments.TotalCount - retrievedCount
|
||||
totalCount := comments.TotalCount + reviews.TotalCount
|
||||
retrievedCount := len(sortedComments)
|
||||
hiddenCount := totalCount - retrievedCount
|
||||
|
||||
if hiddenCount > 0 {
|
||||
fmt.Fprint(&b, cs.Gray(fmt.Sprintf("———————— Not showing %s ————————", utils.Pluralize(hiddenCount, "comment"))))
|
||||
fmt.Fprintf(&b, "\n\n\n")
|
||||
}
|
||||
|
||||
for i, comment := range comments.Nodes {
|
||||
for i, comment := range sortedComments {
|
||||
last := i+1 == retrievedCount
|
||||
cmt, err := formatComment(io, comment, last)
|
||||
if err != nil {
|
||||
|
|
@ -61,18 +80,21 @@ func CommentList(io *iostreams.IOStreams, comments api.Comments) (string, error)
|
|||
return b.String(), nil
|
||||
}
|
||||
|
||||
func formatComment(io *iostreams.IOStreams, comment api.Comment, newest bool) (string, error) {
|
||||
func formatComment(io *iostreams.IOStreams, comment Comment, newest bool) (string, error) {
|
||||
var b strings.Builder
|
||||
cs := io.ColorScheme()
|
||||
|
||||
// Header
|
||||
fmt.Fprint(&b, cs.Bold(comment.Author.Login))
|
||||
if comment.AuthorAssociation != "NONE" {
|
||||
fmt.Fprint(&b, cs.Bold(fmt.Sprintf(" (%s)", strings.ToLower(comment.AuthorAssociation))))
|
||||
fmt.Fprint(&b, cs.Bold(comment.AuthorLogin()))
|
||||
if comment.Status() != "" {
|
||||
fmt.Fprint(&b, formatCommentStatus(cs, comment.Status()))
|
||||
}
|
||||
fmt.Fprint(&b, cs.Bold(fmt.Sprintf(" • %s", utils.FuzzyAgoAbbr(time.Now(), comment.CreatedAt))))
|
||||
if comment.IncludesCreatedEdit {
|
||||
fmt.Fprint(&b, cs.Bold(" • edited"))
|
||||
if comment.Association() != "NONE" {
|
||||
fmt.Fprint(&b, cs.Bold(fmt.Sprintf(" (%s)", strings.Title(strings.ToLower(comment.Association())))))
|
||||
}
|
||||
fmt.Fprint(&b, cs.Bold(fmt.Sprintf(" • %s", utils.FuzzyAgoAbbr(time.Now(), comment.Created()))))
|
||||
if comment.IsEdited() {
|
||||
fmt.Fprint(&b, cs.Bold(" • Edited"))
|
||||
}
|
||||
if newest {
|
||||
fmt.Fprint(&b, cs.Bold(" • "))
|
||||
|
|
@ -81,20 +103,82 @@ func formatComment(io *iostreams.IOStreams, comment api.Comment, newest bool) (s
|
|||
fmt.Fprintln(&b)
|
||||
|
||||
// Reactions
|
||||
if reactions := ReactionGroupList(comment.ReactionGroups); reactions != "" {
|
||||
if reactions := ReactionGroupList(comment.Reactions()); reactions != "" {
|
||||
fmt.Fprint(&b, reactions)
|
||||
fmt.Fprintln(&b)
|
||||
}
|
||||
|
||||
// Body
|
||||
if comment.Body != "" {
|
||||
var md string
|
||||
var err error
|
||||
if comment.Content() == "" {
|
||||
md = fmt.Sprintf("\n %s\n\n", cs.Gray("No body provided"))
|
||||
} else {
|
||||
style := markdown.GetStyle(io.TerminalTheme())
|
||||
md, err := markdown.Render(comment.Body, style, "")
|
||||
md, err = markdown.Render(comment.Content(), style, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Fprint(&b, md)
|
||||
}
|
||||
fmt.Fprint(&b, md)
|
||||
|
||||
// Footer
|
||||
if comment.Link() != "" {
|
||||
fmt.Fprintf(&b, cs.Gray("View the full review: %s\n\n"), comment.Link())
|
||||
}
|
||||
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
func sortComments(cs api.Comments, rs api.PullRequestReviews) []Comment {
|
||||
comments := cs.Nodes
|
||||
reviews := rs.Nodes
|
||||
var sorted []Comment = make([]Comment, len(comments)+len(reviews))
|
||||
|
||||
var i int
|
||||
for _, c := range comments {
|
||||
sorted[i] = c
|
||||
i++
|
||||
}
|
||||
for _, r := range reviews {
|
||||
sorted[i] = r
|
||||
i++
|
||||
}
|
||||
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
return sorted[i].Created().Before(sorted[j].Created())
|
||||
})
|
||||
|
||||
return sorted
|
||||
}
|
||||
|
||||
const (
|
||||
approvedStatus = "APPROVED"
|
||||
changesRequestedStatus = "CHANGES_REQUESTED"
|
||||
commentedStatus = "COMMENTED"
|
||||
dismissedStatus = "DISMISSED"
|
||||
)
|
||||
|
||||
func formatCommentStatus(cs *iostreams.ColorScheme, status string) string {
|
||||
switch status {
|
||||
case approvedStatus:
|
||||
return fmt.Sprintf(" %s", cs.Green("approved"))
|
||||
case changesRequestedStatus:
|
||||
return fmt.Sprintf(" %s", cs.Red("requested changes"))
|
||||
case commentedStatus, dismissedStatus:
|
||||
return fmt.Sprintf(" %s", strings.ToLower(status))
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func formatRawCommentStatus(status string) string {
|
||||
if status == approvedStatus ||
|
||||
status == changesRequestedStatus ||
|
||||
status == commentedStatus ||
|
||||
status == dismissedStatus {
|
||||
return strings.ReplaceAll(strings.ToLower(status), "_", " ")
|
||||
}
|
||||
|
||||
return "none"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,8 +81,7 @@ func AddMetadataToIssueParams(client *api.Client, baseRepo ghrepo.Interface, par
|
|||
return nil
|
||||
}
|
||||
|
||||
err := fillMetadata(client, baseRepo, tb)
|
||||
if err != nil {
|
||||
if err := fillMetadata(client, baseRepo, tb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -191,3 +190,48 @@ func quoteValueForQuery(v string) string {
|
|||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// MeReplacer resolves usages of `@me` to the handle of the currently logged in user.
|
||||
type MeReplacer struct {
|
||||
apiClient *api.Client
|
||||
hostname string
|
||||
login string
|
||||
}
|
||||
|
||||
func NewMeReplacer(apiClient *api.Client, hostname string) *MeReplacer {
|
||||
return &MeReplacer{
|
||||
apiClient: apiClient,
|
||||
hostname: hostname,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *MeReplacer) currentLogin() (string, error) {
|
||||
if r.login != "" {
|
||||
return r.login, nil
|
||||
}
|
||||
login, err := api.CurrentLoginName(r.apiClient, r.hostname)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed resolving `@me` to your user handle: %w", err)
|
||||
}
|
||||
r.login = login
|
||||
return login, nil
|
||||
}
|
||||
|
||||
func (r *MeReplacer) Replace(handle string) (string, error) {
|
||||
if handle == "@me" {
|
||||
return r.currentLogin()
|
||||
}
|
||||
return handle, nil
|
||||
}
|
||||
|
||||
func (r *MeReplacer) ReplaceSlice(handles []string) ([]string, error) {
|
||||
res := make([]string, len(handles))
|
||||
for i, h := range handles {
|
||||
var err error
|
||||
res[i], err = r.Replace(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
package shared
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
)
|
||||
|
||||
func Test_listURLWithQuery(t *testing.T) {
|
||||
type args struct {
|
||||
|
|
@ -69,3 +77,77 @@ func Test_listURLWithQuery(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeReplacer_Replace(t *testing.T) {
|
||||
rtSuccess := &httpmock.Registry{}
|
||||
rtSuccess.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": {
|
||||
"viewer": { "login": "ResolvedLogin" }
|
||||
} }
|
||||
`),
|
||||
)
|
||||
|
||||
rtFailure := &httpmock.Registry{}
|
||||
rtFailure.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StatusStringResponse(500, `
|
||||
{ "data": {
|
||||
"viewer": { }
|
||||
} }
|
||||
`),
|
||||
)
|
||||
|
||||
type args struct {
|
||||
logins []string
|
||||
client *api.Client
|
||||
repo ghrepo.Interface
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
verify func(t httpmock.Testing)
|
||||
want []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "succeeds resolving the userlogin",
|
||||
args: args{
|
||||
client: api.NewClientFromHTTP(&http.Client{Transport: rtSuccess}),
|
||||
repo: ghrepo.New("OWNER", "REPO"),
|
||||
logins: []string{"some", "@me", "other"},
|
||||
},
|
||||
verify: rtSuccess.Verify,
|
||||
want: []string{"some", "ResolvedLogin", "other"},
|
||||
},
|
||||
{
|
||||
name: "fails resolving the userlogin",
|
||||
args: args{
|
||||
client: api.NewClientFromHTTP(&http.Client{Transport: rtFailure}),
|
||||
repo: ghrepo.New("OWNER", "REPO"),
|
||||
logins: []string{"some", "@me", "other"},
|
||||
},
|
||||
verify: rtFailure.Verify,
|
||||
want: []string(nil),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
me := NewMeReplacer(tt.args.client, tt.args.repo.RepoHost())
|
||||
got, err := me.ReplaceSlice(tt.args.logins)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ReplaceAtMeLogin() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("ReplaceAtMeLogin() = %v, want %v", got, tt.want)
|
||||
}
|
||||
|
||||
if tt.verify != nil {
|
||||
tt.verify(t)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ func Test_PreserveInput(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
if tt.wantPreservation {
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, errOut.String(), tt.wantErrLine)
|
||||
preserved := &IssueMetadataState{}
|
||||
assert.NoError(t, json.Unmarshal(data, preserved))
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@
|
|||
},
|
||||
"authorAssociation": "CONTRIBUTOR",
|
||||
"body": "Comment 2",
|
||||
"createdAt": "2020-01-01T12:00:00Z",
|
||||
"createdAt": "2020-01-03T12:00:00Z",
|
||||
"includesCreatedEdit": false,
|
||||
"reactionGroups": [
|
||||
{
|
||||
|
|
@ -128,7 +128,7 @@
|
|||
},
|
||||
"authorAssociation": "MEMBER",
|
||||
"body": "Comment 3",
|
||||
"createdAt": "2020-01-01T12:00:00Z",
|
||||
"createdAt": "2020-01-05T12:00:00Z",
|
||||
"includesCreatedEdit": false,
|
||||
"reactionGroups": [
|
||||
{
|
||||
|
|
@ -187,7 +187,7 @@
|
|||
},
|
||||
"authorAssociation": "OWNER",
|
||||
"body": "Comment 4",
|
||||
"createdAt": "2020-01-01T12:00:00Z",
|
||||
"createdAt": "2020-01-07T12:00:00Z",
|
||||
"includesCreatedEdit": false,
|
||||
"reactionGroups": [
|
||||
{
|
||||
|
|
@ -246,7 +246,7 @@
|
|||
},
|
||||
"authorAssociation": "COLLABORATOR",
|
||||
"body": "Comment 5",
|
||||
"createdAt": "2020-01-01T12:00:00Z",
|
||||
"createdAt": "2020-01-09T12:00:00Z",
|
||||
"includesCreatedEdit": false,
|
||||
"reactionGroups": [
|
||||
{
|
||||
|
|
|
|||
67
pkg/cmd/pr/view/fixtures/prViewPreviewManyReviews.json
Normal file
67
pkg/cmd/pr/view/fixtures/prViewPreviewManyReviews.json
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"data": {
|
||||
"repository": {
|
||||
"pullRequest": {
|
||||
"reviews": {
|
||||
"nodes": [
|
||||
{
|
||||
"author": {
|
||||
"login": "123"
|
||||
},
|
||||
"state": "COMMENTED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "def"
|
||||
},
|
||||
"state": "CHANGES_REQUESTED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "abc"
|
||||
},
|
||||
"state": "APPROVED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "DEF"
|
||||
},
|
||||
"state": "COMMENTED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "xyz"
|
||||
},
|
||||
"state": "APPROVED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": ""
|
||||
},
|
||||
"state": "APPROVED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"state": "CHANGES_REQUESTED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"state": "DISMISSED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "monalisa"
|
||||
},
|
||||
"state": "PENDING"
|
||||
}
|
||||
],
|
||||
"totalCount": 9
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
pkg/cmd/pr/view/fixtures/prViewPreviewNoReviews.json
Normal file
1
pkg/cmd/pr/view/fixtures/prViewPreviewNoReviews.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{ "data": { "repository": { "pullRequest": { "reviews": { } } } } }
|
||||
318
pkg/cmd/pr/view/fixtures/prViewPreviewReviews.json
Normal file
318
pkg/cmd/pr/view/fixtures/prViewPreviewReviews.json
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
{
|
||||
"data": {
|
||||
"repository": {
|
||||
"pullRequest": {
|
||||
"reviews": {
|
||||
"nodes": [
|
||||
{
|
||||
"author": {
|
||||
"login": "sam"
|
||||
},
|
||||
"authorAssociation": "NONE",
|
||||
"body": "Review 1",
|
||||
"createdAt": "2020-01-02T12:00:00Z",
|
||||
"includesCreatedEdit": false,
|
||||
"reactionGroups": [
|
||||
{
|
||||
"content": "CONFUSED",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "EYES",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "HEART",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "HOORAY",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "LAUGH",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "ROCKET",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "THUMBS_DOWN",
|
||||
"users": {
|
||||
"totalCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "THUMBS_UP",
|
||||
"users": {
|
||||
"totalCount": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"state": "COMMENTED",
|
||||
"url": "https://github.com/OWNER/REPO/pull/12#pullrequestreview-1"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "matt"
|
||||
},
|
||||
"authorAssociation": "OWNER",
|
||||
"body": "Review 2",
|
||||
"createdAt": "2020-01-04T12:00:00Z",
|
||||
"includesCreatedEdit": false,
|
||||
"reactionGroups": [
|
||||
{
|
||||
"content": "CONFUSED",
|
||||
"users": {
|
||||
"totalCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "EYES",
|
||||
"users": {
|
||||
"totalCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "HEART",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "HOORAY",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "LAUGH",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "ROCKET",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "THUMBS_DOWN",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "THUMBS_UP",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"state": "CHANGES_REQUESTED",
|
||||
"url": "https://github.com/OWNER/REPO/pull/12#pullrequestreview-2"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "leah"
|
||||
},
|
||||
"authorAssociation": "MEMBER",
|
||||
"body": "Review 3",
|
||||
"createdAt": "2020-01-06T12:00:00Z",
|
||||
"includesCreatedEdit": true,
|
||||
"reactionGroups": [
|
||||
{
|
||||
"content": "CONFUSED",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "EYES",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "HEART",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "HOORAY",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "LAUGH",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "ROCKET",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "THUMBS_DOWN",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "THUMBS_UP",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"state": "APPROVED",
|
||||
"url": "https://github.com/OWNER/REPO/pull/12#pullrequestreview-3"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "louise"
|
||||
},
|
||||
"authorAssociation": "NONE",
|
||||
"body": "Review 4",
|
||||
"createdAt": "2020-01-08T12:00:00Z",
|
||||
"includesCreatedEdit": false,
|
||||
"reactionGroups": [
|
||||
{
|
||||
"content": "CONFUSED",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "EYES",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "HEART",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "HOORAY",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "LAUGH",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "ROCKET",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "THUMBS_DOWN",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "THUMBS_UP",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"state": "DISMISSED",
|
||||
"url": "https://github.com/OWNER/REPO/pull/12#pullrequestreview-4"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "david"
|
||||
},
|
||||
"authorAssociation": "NONE",
|
||||
"body": "Review 5",
|
||||
"createdAt": "2020-01-10T12:00:00Z",
|
||||
"includesCreatedEdit": false,
|
||||
"reactionGroups": [
|
||||
{
|
||||
"content": "CONFUSED",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "EYES",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "HEART",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "HOORAY",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "LAUGH",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "ROCKET",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "THUMBS_DOWN",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "THUMBS_UP",
|
||||
"users": {
|
||||
"totalCount": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"state": "PENDING",
|
||||
"url": "https://github.com/OWNER/REPO/pull/12#pullrequestreview-5"
|
||||
}
|
||||
],
|
||||
"totalCount": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -93,7 +93,7 @@
|
|||
},
|
||||
"authorAssociation": "COLLABORATOR",
|
||||
"body": "Comment 5",
|
||||
"createdAt": "2020-01-01T12:00:00Z",
|
||||
"createdAt": "2020-01-09T12:00:00Z",
|
||||
"includesCreatedEdit": false,
|
||||
"reactionGroups": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -21,28 +21,6 @@
|
|||
],
|
||||
"totalcount": 1
|
||||
},
|
||||
"reviews": {
|
||||
"nodes": [
|
||||
{
|
||||
"author": {
|
||||
"login": "3"
|
||||
},
|
||||
"state": "COMMENTED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "2"
|
||||
},
|
||||
"state": "APPROVED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "1"
|
||||
},
|
||||
"state": "CHANGES_REQUESTED"
|
||||
}
|
||||
]
|
||||
},
|
||||
"assignees": {
|
||||
"nodes": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -33,64 +33,6 @@
|
|||
],
|
||||
"totalcount": 1
|
||||
},
|
||||
"reviews": {
|
||||
"nodes": [
|
||||
{
|
||||
"author": {
|
||||
"login": "123"
|
||||
},
|
||||
"state": "COMMENTED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "def"
|
||||
},
|
||||
"state": "CHANGES_REQUESTED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "abc"
|
||||
},
|
||||
"state": "APPROVED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "DEF"
|
||||
},
|
||||
"state": "COMMENTED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "xyz"
|
||||
},
|
||||
"state": "APPROVED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": ""
|
||||
},
|
||||
"state": "APPROVED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"state": "CHANGES_REQUESTED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"state": "DISMISSED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "monalisa"
|
||||
},
|
||||
"state": "PENDING"
|
||||
}
|
||||
]
|
||||
},
|
||||
"assignees": {
|
||||
"nodes": [],
|
||||
"totalcount": 0
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/api"
|
||||
|
|
@ -80,13 +81,9 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
|
|||
}
|
||||
|
||||
func viewRun(opts *ViewOptions) error {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
pr, repo, err := shared.PRFromArgs(apiClient, opts.BaseRepo, opts.Branch, opts.Remotes, opts.SelectorArg)
|
||||
opts.IO.StartProgressIndicator()
|
||||
pr, err := retrievePullRequest(opts)
|
||||
opts.IO.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -101,16 +98,6 @@ func viewRun(opts *ViewOptions) error {
|
|||
return utils.OpenInBrowser(openURL)
|
||||
}
|
||||
|
||||
if opts.Comments {
|
||||
opts.IO.StartProgressIndicator()
|
||||
comments, err := api.CommentsForPullRequest(apiClient, repo, pr)
|
||||
opts.IO.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pr.Comments = *comments
|
||||
}
|
||||
|
||||
opts.IO.DetectTerminalTheme()
|
||||
|
||||
err = opts.IO.StartPager()
|
||||
|
|
@ -120,11 +107,11 @@ func viewRun(opts *ViewOptions) error {
|
|||
defer opts.IO.StopPager()
|
||||
|
||||
if connectedToTerminal {
|
||||
return printHumanPrPreview(opts.IO, pr)
|
||||
return printHumanPrPreview(opts, pr)
|
||||
}
|
||||
|
||||
if opts.Comments {
|
||||
fmt.Fprint(opts.IO.Out, shared.RawCommentList(pr.Comments))
|
||||
fmt.Fprint(opts.IO.Out, shared.RawCommentList(pr.Comments, pr.Reviews))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -157,9 +144,9 @@ func printRawPrPreview(io *iostreams.IOStreams, pr *api.PullRequest) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func printHumanPrPreview(io *iostreams.IOStreams, pr *api.PullRequest) error {
|
||||
out := io.Out
|
||||
cs := io.ColorScheme()
|
||||
func printHumanPrPreview(opts *ViewOptions, pr *api.PullRequest) error {
|
||||
out := opts.IO.Out
|
||||
cs := opts.IO.ColorScheme()
|
||||
|
||||
// Header (Title and State)
|
||||
fmt.Fprintln(out, cs.Bold(pr.Title))
|
||||
|
|
@ -201,21 +188,23 @@ func printHumanPrPreview(io *iostreams.IOStreams, pr *api.PullRequest) error {
|
|||
}
|
||||
|
||||
// Body
|
||||
fmt.Fprintln(out)
|
||||
var md string
|
||||
var err error
|
||||
if pr.Body == "" {
|
||||
pr.Body = "_No description provided_"
|
||||
md = fmt.Sprintf("\n %s\n\n", cs.Gray("No description provided"))
|
||||
} else {
|
||||
style := markdown.GetStyle(opts.IO.TerminalTheme())
|
||||
md, err = markdown.Render(pr.Body, style, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
style := markdown.GetStyle(io.TerminalTheme())
|
||||
md, err := markdown.Render(pr.Body, style, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprint(out, md)
|
||||
fmt.Fprintln(out)
|
||||
fmt.Fprintf(out, "\n%s\n", md)
|
||||
|
||||
// Comments
|
||||
if pr.Comments.TotalCount > 0 {
|
||||
comments, err := shared.CommentList(io, pr.Comments)
|
||||
// Reviews and Comments
|
||||
if pr.Comments.TotalCount > 0 || pr.Reviews.TotalCount > 0 {
|
||||
preview := !opts.Comments
|
||||
comments, err := shared.CommentList(opts.IO, pr.Comments, pr.DisplayableReviews(), preview)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -405,3 +394,51 @@ func prStateWithDraft(pr *api.PullRequest) string {
|
|||
|
||||
return pr.State
|
||||
}
|
||||
|
||||
func retrievePullRequest(opts *ViewOptions) (*api.PullRequest, error) {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
pr, repo, err := shared.PRFromArgs(apiClient, opts.BaseRepo, opts.Branch, opts.Remotes, opts.SelectorArg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if opts.BrowserMode {
|
||||
return pr, nil
|
||||
}
|
||||
|
||||
var errp, errc error
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var reviews *api.PullRequestReviews
|
||||
reviews, errp = api.ReviewsForPullRequest(apiClient, repo, pr)
|
||||
pr.Reviews = *reviews
|
||||
}()
|
||||
|
||||
if opts.Comments {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var comments *api.Comments
|
||||
comments, errc = api.CommentsForPullRequest(apiClient, repo, pr)
|
||||
pr.Comments = *comments
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if errp != nil {
|
||||
err = errp
|
||||
}
|
||||
if errc != nil {
|
||||
err = errc
|
||||
}
|
||||
return pr, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/briandowns/spinner"
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/config"
|
||||
|
|
@ -19,7 +18,6 @@ import (
|
|||
"github.com/cli/cli/pkg/httpmock"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/test"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -168,13 +166,16 @@ func TestPRView_Preview_nontty(t *testing.T) {
|
|||
tests := map[string]struct {
|
||||
branch string
|
||||
args string
|
||||
fixture string
|
||||
fixtures map[string]string
|
||||
expectedOutputs []string
|
||||
}{
|
||||
"Open PR without metadata": {
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixture: "./fixtures/prViewPreview.json",
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreview.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`title:\tBlueberries are from a fork\n`,
|
||||
`state:\tOPEN\n`,
|
||||
|
|
@ -190,12 +191,15 @@ func TestPRView_Preview_nontty(t *testing.T) {
|
|||
},
|
||||
},
|
||||
"Open PR with metadata by number": {
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixture: "./fixtures/prViewPreviewWithMetadataByNumber.json",
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewWithMetadataByNumber.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`title:\tBlueberries are from a fork\n`,
|
||||
`reviewers:\t2 \(Approved\), 3 \(Commented\), 1 \(Requested\)\n`,
|
||||
`reviewers:\t1 \(Requested\)\n`,
|
||||
`assignees:\tmarseilles, monaco\n`,
|
||||
`labels:\tone, two, three, four, five\n`,
|
||||
`projects:\tProject 1 \(column A\), Project 2 \(column B\), Project 3 \(column C\), Project 4 \(Awaiting triage\)\n`,
|
||||
|
|
@ -204,9 +208,12 @@ func TestPRView_Preview_nontty(t *testing.T) {
|
|||
},
|
||||
},
|
||||
"Open PR with reviewers by number": {
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixture: "./fixtures/prViewPreviewWithReviewersByNumber.json",
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewWithReviewersByNumber.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewManyReviews.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`title:\tBlueberries are from a fork\n`,
|
||||
`state:\tOPEN\n`,
|
||||
|
|
@ -220,14 +227,18 @@ func TestPRView_Preview_nontty(t *testing.T) {
|
|||
},
|
||||
},
|
||||
"Open PR with metadata by branch": {
|
||||
branch: "master",
|
||||
args: "blueberries",
|
||||
fixture: "./fixtures/prViewPreviewWithMetadataByBranch.json",
|
||||
branch: "master",
|
||||
args: "blueberries",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestForBranch": "./fixtures/prViewPreviewWithMetadataByBranch.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`title:\tBlueberries are a good fruit`,
|
||||
`state:\tOPEN`,
|
||||
`author:\tnobody`,
|
||||
`assignees:\tmarseilles, monaco\n`,
|
||||
`reviewers:\t\n`,
|
||||
`labels:\tone, two, three, four, five\n`,
|
||||
`projects:\tProject 1 \(column A\), Project 2 \(column B\), Project 3 \(column C\)\n`,
|
||||
`milestone:\tuluru\n`,
|
||||
|
|
@ -235,14 +246,18 @@ func TestPRView_Preview_nontty(t *testing.T) {
|
|||
},
|
||||
},
|
||||
"Open PR for the current branch": {
|
||||
branch: "blueberries",
|
||||
args: "",
|
||||
fixture: "./fixtures/prView.json",
|
||||
branch: "blueberries",
|
||||
args: "",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestForBranch": "./fixtures/prView.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`title:\tBlueberries are a good fruit`,
|
||||
`state:\tOPEN`,
|
||||
`author:\tnobody`,
|
||||
`assignees:\t\n`,
|
||||
`reviewers:\t\n`,
|
||||
`labels:\t\n`,
|
||||
`projects:\t\n`,
|
||||
`milestone:\t\n`,
|
||||
|
|
@ -250,23 +265,30 @@ func TestPRView_Preview_nontty(t *testing.T) {
|
|||
},
|
||||
},
|
||||
"Open PR wth empty body for the current branch": {
|
||||
branch: "blueberries",
|
||||
args: "",
|
||||
fixture: "./fixtures/prView_EmptyBody.json",
|
||||
branch: "blueberries",
|
||||
args: "",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestForBranch": "./fixtures/prView_EmptyBody.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`title:\tBlueberries are a good fruit`,
|
||||
`state:\tOPEN`,
|
||||
`author:\tnobody`,
|
||||
`assignees:\t\n`,
|
||||
`reviewers:\t\n`,
|
||||
`labels:\t\n`,
|
||||
`projects:\t\n`,
|
||||
`milestone:\t\n`,
|
||||
},
|
||||
},
|
||||
"Closed PR": {
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixture: "./fixtures/prViewPreviewClosedState.json",
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewClosedState.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`state:\tCLOSED\n`,
|
||||
`author:\tnobody\n`,
|
||||
|
|
@ -279,9 +301,12 @@ func TestPRView_Preview_nontty(t *testing.T) {
|
|||
},
|
||||
},
|
||||
"Merged PR": {
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixture: "./fixtures/prViewPreviewMergedState.json",
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewMergedState.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`state:\tMERGED\n`,
|
||||
`author:\tnobody\n`,
|
||||
|
|
@ -294,30 +319,38 @@ func TestPRView_Preview_nontty(t *testing.T) {
|
|||
},
|
||||
},
|
||||
"Draft PR": {
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixture: "./fixtures/prViewPreviewDraftState.json",
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewDraftState.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`title:\tBlueberries are from a fork\n`,
|
||||
`state:\tDRAFT\n`,
|
||||
`author:\tnobody\n`,
|
||||
`labels:`,
|
||||
`assignees:`,
|
||||
`reviewers:`,
|
||||
`projects:`,
|
||||
`milestone:`,
|
||||
`\*\*blueberries taste good\*\*`,
|
||||
},
|
||||
},
|
||||
"Draft PR by branch": {
|
||||
branch: "master",
|
||||
args: "blueberries",
|
||||
fixture: "./fixtures/prViewPreviewDraftStatebyBranch.json",
|
||||
branch: "master",
|
||||
args: "blueberries",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestForBranch": "./fixtures/prViewPreviewDraftStatebyBranch.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`title:\tBlueberries are a good fruit\n`,
|
||||
`state:\tDRAFT\n`,
|
||||
`author:\tnobody\n`,
|
||||
`labels:`,
|
||||
`assignees:`,
|
||||
`reviewers:`,
|
||||
`projects:`,
|
||||
`milestone:`,
|
||||
`\*\*blueberries taste good\*\*`,
|
||||
|
|
@ -329,7 +362,10 @@ func TestPRView_Preview_nontty(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
http.Register(httpmock.GraphQL(`query PullRequest(ByNumber|ForBranch)\b`), httpmock.FileResponse(tc.fixture))
|
||||
for name, file := range tc.fixtures {
|
||||
name := fmt.Sprintf(`query %s\b`, name)
|
||||
http.Register(httpmock.GraphQL(name), httpmock.FileResponse(file))
|
||||
}
|
||||
|
||||
output, err := runCommand(http, tc.branch, false, tc.args)
|
||||
if err != nil {
|
||||
|
|
@ -338,6 +374,7 @@ func TestPRView_Preview_nontty(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.String(), tc.expectedOutputs...)
|
||||
})
|
||||
}
|
||||
|
|
@ -347,13 +384,16 @@ func TestPRView_Preview(t *testing.T) {
|
|||
tests := map[string]struct {
|
||||
branch string
|
||||
args string
|
||||
fixture string
|
||||
fixtures map[string]string
|
||||
expectedOutputs []string
|
||||
}{
|
||||
"Open PR without metadata": {
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixture: "./fixtures/prViewPreview.json",
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreview.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`Blueberries are from a fork`,
|
||||
`Open.*nobody wants to merge 12 commits into master from blueberries`,
|
||||
|
|
@ -362,13 +402,16 @@ func TestPRView_Preview(t *testing.T) {
|
|||
},
|
||||
},
|
||||
"Open PR with metadata by number": {
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixture: "./fixtures/prViewPreviewWithMetadataByNumber.json",
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewWithMetadataByNumber.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`Blueberries are from a fork`,
|
||||
`Open.*nobody wants to merge 12 commits into master from blueberries`,
|
||||
`Reviewers:.*2 \(.*Approved.*\), 3 \(Commented\), 1 \(.*Requested.*\)\n`,
|
||||
`Reviewers:.*1 \(.*Requested.*\)\n`,
|
||||
`Assignees:.*marseilles, monaco\n`,
|
||||
`Labels:.*one, two, three, four, five\n`,
|
||||
`Projects:.*Project 1 \(column A\), Project 2 \(column B\), Project 3 \(column C\), Project 4 \(Awaiting triage\)\n`,
|
||||
|
|
@ -378,9 +421,12 @@ func TestPRView_Preview(t *testing.T) {
|
|||
},
|
||||
},
|
||||
"Open PR with reviewers by number": {
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixture: "./fixtures/prViewPreviewWithReviewersByNumber.json",
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewWithReviewersByNumber.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewManyReviews.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`Blueberries are from a fork`,
|
||||
`Reviewers:.*DEF \(.*Commented.*\), def \(.*Changes requested.*\), ghost \(.*Approved.*\), hubot \(Commented\), xyz \(.*Approved.*\), 123 \(.*Requested.*\), Team 1 \(.*Requested.*\), abc \(.*Requested.*\)\n`,
|
||||
|
|
@ -389,9 +435,12 @@ func TestPRView_Preview(t *testing.T) {
|
|||
},
|
||||
},
|
||||
"Open PR with metadata by branch": {
|
||||
branch: "master",
|
||||
args: "blueberries",
|
||||
fixture: "./fixtures/prViewPreviewWithMetadataByBranch.json",
|
||||
branch: "master",
|
||||
args: "blueberries",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestForBranch": "./fixtures/prViewPreviewWithMetadataByBranch.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`Blueberries are a good fruit`,
|
||||
`Open.*nobody wants to merge 8 commits into master from blueberries`,
|
||||
|
|
@ -404,9 +453,12 @@ func TestPRView_Preview(t *testing.T) {
|
|||
},
|
||||
},
|
||||
"Open PR for the current branch": {
|
||||
branch: "blueberries",
|
||||
args: "",
|
||||
fixture: "./fixtures/prView.json",
|
||||
branch: "blueberries",
|
||||
args: "",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestForBranch": "./fixtures/prView.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`Blueberries are a good fruit`,
|
||||
`Open.*nobody wants to merge 8 commits into master from blueberries`,
|
||||
|
|
@ -415,9 +467,12 @@ func TestPRView_Preview(t *testing.T) {
|
|||
},
|
||||
},
|
||||
"Open PR wth empty body for the current branch": {
|
||||
branch: "blueberries",
|
||||
args: "",
|
||||
fixture: "./fixtures/prView_EmptyBody.json",
|
||||
branch: "blueberries",
|
||||
args: "",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestForBranch": "./fixtures/prView_EmptyBody.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`Blueberries are a good fruit`,
|
||||
`Open.*nobody wants to merge 8 commits into master from blueberries`,
|
||||
|
|
@ -425,9 +480,12 @@ func TestPRView_Preview(t *testing.T) {
|
|||
},
|
||||
},
|
||||
"Closed PR": {
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixture: "./fixtures/prViewPreviewClosedState.json",
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewClosedState.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`Blueberries are from a fork`,
|
||||
`Closed.*nobody wants to merge 12 commits into master from blueberries`,
|
||||
|
|
@ -436,9 +494,12 @@ func TestPRView_Preview(t *testing.T) {
|
|||
},
|
||||
},
|
||||
"Merged PR": {
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixture: "./fixtures/prViewPreviewMergedState.json",
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewMergedState.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`Blueberries are from a fork`,
|
||||
`Merged.*nobody wants to merge 12 commits into master from blueberries`,
|
||||
|
|
@ -447,9 +508,12 @@ func TestPRView_Preview(t *testing.T) {
|
|||
},
|
||||
},
|
||||
"Draft PR": {
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixture: "./fixtures/prViewPreviewDraftState.json",
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewDraftState.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`Blueberries are from a fork`,
|
||||
`Draft.*nobody wants to merge 12 commits into master from blueberries`,
|
||||
|
|
@ -458,9 +522,12 @@ func TestPRView_Preview(t *testing.T) {
|
|||
},
|
||||
},
|
||||
"Draft PR by branch": {
|
||||
branch: "master",
|
||||
args: "blueberries",
|
||||
fixture: "./fixtures/prViewPreviewDraftStatebyBranch.json",
|
||||
branch: "master",
|
||||
args: "blueberries",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestForBranch": "./fixtures/prViewPreviewDraftStatebyBranch.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`Blueberries are a good fruit`,
|
||||
`Draft.*nobody wants to merge 8 commits into master from blueberries`,
|
||||
|
|
@ -474,7 +541,10 @@ func TestPRView_Preview(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
http.Register(httpmock.GraphQL(`query PullRequest(ByNumber|ForBranch)\b`), httpmock.FileResponse(tc.fixture))
|
||||
for name, file := range tc.fixtures {
|
||||
name := fmt.Sprintf(`query %s\b`, name)
|
||||
http.Register(httpmock.GraphQL(name), httpmock.FileResponse(file))
|
||||
}
|
||||
|
||||
output, err := runCommand(http, tc.branch, true, tc.args)
|
||||
if err != nil {
|
||||
|
|
@ -483,6 +553,7 @@ func TestPRView_Preview(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.String(), tc.expectedOutputs...)
|
||||
})
|
||||
}
|
||||
|
|
@ -494,6 +565,7 @@ func TestPRView_web_currentBranch(t *testing.T) {
|
|||
http.Register(httpmock.GraphQL(`query PullRequestForBranch\b`), httpmock.FileResponse("./fixtures/prView.json"))
|
||||
|
||||
var seenCmd *exec.Cmd
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case `git config --get-regexp ^branch\.blueberries\.(remote|merge)$`:
|
||||
|
|
@ -528,6 +600,7 @@ func TestPRView_web_noResultsForBranch(t *testing.T) {
|
|||
http.Register(httpmock.GraphQL(`query PullRequestForBranch\b`), httpmock.FileResponse("./fixtures/prView_NoActiveBranch.json"))
|
||||
|
||||
var seenCmd *exec.Cmd
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case `git config --get-regexp ^branch\.blueberries\.(remote|merge)$`:
|
||||
|
|
@ -562,6 +635,7 @@ func TestPRView_web_numberArg(t *testing.T) {
|
|||
)
|
||||
|
||||
var seenCmd *exec.Cmd
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmd = cmd
|
||||
return &test.OutputStub{}
|
||||
|
|
@ -595,6 +669,7 @@ func TestPRView_web_numberArgWithHash(t *testing.T) {
|
|||
)
|
||||
|
||||
var seenCmd *exec.Cmd
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmd = cmd
|
||||
return &test.OutputStub{}
|
||||
|
|
@ -628,6 +703,7 @@ func TestPRView_web_urlArg(t *testing.T) {
|
|||
)
|
||||
|
||||
var seenCmd *exec.Cmd
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmd = cmd
|
||||
return &test.OutputStub{}
|
||||
|
|
@ -663,6 +739,7 @@ func TestPRView_web_branchArg(t *testing.T) {
|
|||
)
|
||||
|
||||
var seenCmd *exec.Cmd
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmd = cmd
|
||||
return &test.OutputStub{}
|
||||
|
|
@ -699,6 +776,7 @@ func TestPRView_web_branchWithOwnerArg(t *testing.T) {
|
|||
)
|
||||
|
||||
var seenCmd *exec.Cmd
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmd = cmd
|
||||
return &test.OutputStub{}
|
||||
|
|
@ -731,14 +809,15 @@ func TestPRView_tty_Comments(t *testing.T) {
|
|||
branch: "master",
|
||||
cli: "123",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewSingleComment.json",
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewSingleComment.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewReviews.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`some title`,
|
||||
`1 \x{1f615} • 2 \x{1f440} • 3 \x{2764}\x{fe0f}`,
|
||||
`some body`,
|
||||
`———————— Not showing 4 comments ————————`,
|
||||
`marseilles \(collaborator\) • Jan 1, 2020 • Newest comment`,
|
||||
`———————— Not showing 8 comments ————————`,
|
||||
`marseilles \(Collaborator\) • Jan 9, 2020 • Newest comment`,
|
||||
`4 \x{1f389} • 5 \x{1f604} • 6 \x{1f680}`,
|
||||
`Comment 5`,
|
||||
`Use --comments to view the full conversation`,
|
||||
|
|
@ -750,21 +829,36 @@ func TestPRView_tty_Comments(t *testing.T) {
|
|||
cli: "123 --comments",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewSingleComment.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewReviews.json",
|
||||
"CommentsForPullRequest": "./fixtures/prViewPreviewFullComments.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`some title`,
|
||||
`some body`,
|
||||
`monalisa • Jan 1, 2020 • edited`,
|
||||
`monalisa • Jan 1, 2020 • Edited`,
|
||||
`1 \x{1f615} • 2 \x{1f440} • 3 \x{2764}\x{fe0f} • 4 \x{1f389} • 5 \x{1f604} • 6 \x{1f680} • 7 \x{1f44e} • 8 \x{1f44d}`,
|
||||
`Comment 1`,
|
||||
`johnnytest \(contributor\) • Jan 1, 2020`,
|
||||
`sam commented • Jan 2, 2020`,
|
||||
`1 \x{1f44e} • 1 \x{1f44d}`,
|
||||
`Review 1`,
|
||||
`View the full review: https://github.com/OWNER/REPO/pull/12#pullrequestreview-1`,
|
||||
`johnnytest \(Contributor\) • Jan 3, 2020`,
|
||||
`Comment 2`,
|
||||
`elvisp \(member\) • Jan 1, 2020`,
|
||||
`matt requested changes \(Owner\) • Jan 4, 2020`,
|
||||
`1 \x{1f615} • 1 \x{1f440}`,
|
||||
`Review 2`,
|
||||
`View the full review: https://github.com/OWNER/REPO/pull/12#pullrequestreview-2`,
|
||||
`elvisp \(Member\) • Jan 5, 2020`,
|
||||
`Comment 3`,
|
||||
`loislane \(owner\) • Jan 1, 2020`,
|
||||
`leah approved \(Member\) • Jan 6, 2020 • Edited`,
|
||||
`Review 3`,
|
||||
`View the full review: https://github.com/OWNER/REPO/pull/12#pullrequestreview-3`,
|
||||
`loislane \(Owner\) • Jan 7, 2020`,
|
||||
`Comment 4`,
|
||||
`marseilles \(collaborator\) • Jan 1, 2020 • Newest comment`,
|
||||
`louise dismissed • Jan 8, 2020`,
|
||||
`Review 4`,
|
||||
`View the full review: https://github.com/OWNER/REPO/pull/12#pullrequestreview-4`,
|
||||
`marseilles \(Collaborator\) • Jan 9, 2020 • Newest comment`,
|
||||
`Comment 5`,
|
||||
`View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12`,
|
||||
},
|
||||
|
|
@ -777,7 +871,6 @@ func TestPRView_tty_Comments(t *testing.T) {
|
|||
}
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
stubSpinner()
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
for name, file := range tt.fixtures {
|
||||
|
|
@ -791,6 +884,7 @@ func TestPRView_tty_Comments(t *testing.T) {
|
|||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.String(), tt.expectedOutputs...)
|
||||
})
|
||||
}
|
||||
|
|
@ -808,7 +902,8 @@ func TestPRView_nontty_Comments(t *testing.T) {
|
|||
branch: "master",
|
||||
cli: "123",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewSingleComment.json",
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewSingleComment.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewReviews.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`title:\tsome title`,
|
||||
|
|
@ -823,28 +918,54 @@ func TestPRView_nontty_Comments(t *testing.T) {
|
|||
cli: "123 --comments",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewSingleComment.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewReviews.json",
|
||||
"CommentsForPullRequest": "./fixtures/prViewPreviewFullComments.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`author:\tmonalisa`,
|
||||
`association:\t`,
|
||||
`association:\tnone`,
|
||||
`edited:\ttrue`,
|
||||
`status:\tnone`,
|
||||
`Comment 1`,
|
||||
`author:\tsam`,
|
||||
`association:\tnone`,
|
||||
`edited:\tfalse`,
|
||||
`status:\tcommented`,
|
||||
`Review 1`,
|
||||
`author:\tjohnnytest`,
|
||||
`association:\tcontributor`,
|
||||
`edited:\tfalse`,
|
||||
`status:\tnone`,
|
||||
`Comment 2`,
|
||||
`author:\tmatt`,
|
||||
`association:\towner`,
|
||||
`edited:\tfalse`,
|
||||
`status:\tchanges requested`,
|
||||
`Review 2`,
|
||||
`author:\telvisp`,
|
||||
`association:\tmember`,
|
||||
`edited:\tfalse`,
|
||||
`status:\tnone`,
|
||||
`Comment 3`,
|
||||
`author:\tleah`,
|
||||
`association:\tmember`,
|
||||
`edited:\ttrue`,
|
||||
`status:\tapproved`,
|
||||
`Review 3`,
|
||||
`author:\tloislane`,
|
||||
`association:\towner`,
|
||||
`edited:\tfalse`,
|
||||
`status:\tnone`,
|
||||
`Comment 4`,
|
||||
`author:\tlouise`,
|
||||
`association:\tnone`,
|
||||
`edited:\tfalse`,
|
||||
`status:\tdismissed`,
|
||||
`Review 4`,
|
||||
`author:\tmarseilles`,
|
||||
`association:\tcollaborator`,
|
||||
`edited:\tfalse`,
|
||||
`status:\tnone`,
|
||||
`Comment 5`,
|
||||
},
|
||||
},
|
||||
|
|
@ -869,12 +990,8 @@ func TestPRView_nontty_Comments(t *testing.T) {
|
|||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.String(), tt.expectedOutputs...)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func stubSpinner() {
|
||||
utils.StartSpinner = func(_ *spinner.Spinner) {}
|
||||
utils.StopSpinner = func(_ *spinner.Spinner) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ func cloneRun(opts *CloneOptions) error {
|
|||
}
|
||||
upstreamURL := ghrepo.FormatRemoteURL(canonicalRepo.Parent, protocol)
|
||||
|
||||
err = git.AddUpstreamRemote(upstreamURL, cloneDir)
|
||||
err = git.AddUpstreamRemote(upstreamURL, cloneDir, []string{canonicalRepo.Parent.DefaultBranchRef.Name})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/run"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
|
|
@ -77,11 +78,11 @@ func TestNewCmdClone(t *testing.T) {
|
|||
cmd.SetErr(stderr)
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
if err != nil {
|
||||
assert.Equal(t, tt.wantErr, err.Error())
|
||||
if tt.wantErr != "" {
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
return
|
||||
} else if tt.wantErr != "" {
|
||||
t.Errorf("expected error %q, got nil", tt.wantErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", stdout.String())
|
||||
|
|
@ -182,6 +183,7 @@ func Test_RepoClone(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query RepositoryInfo\b`),
|
||||
httpmock.StringResponse(`
|
||||
|
|
@ -196,10 +198,11 @@ func Test_RepoClone(t *testing.T) {
|
|||
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
cs, restore := test.InitCmdStubber()
|
||||
defer restore()
|
||||
|
||||
cs.Stub("") // git clone
|
||||
cs, restore := run.Stub()
|
||||
defer restore(t)
|
||||
cs.Register(`git clone`, 0, "", func(s []string) {
|
||||
assert.Equal(t, tt.want, strings.Join(s, " "))
|
||||
})
|
||||
|
||||
output, err := runCloneCommand(httpClient, tt.args)
|
||||
if err != nil {
|
||||
|
|
@ -208,9 +211,6 @@ func Test_RepoClone(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
assert.Equal(t, 1, cs.Count)
|
||||
assert.Equal(t, tt.want, strings.Join(cs.Calls[0].Args, " "))
|
||||
reg.Verify(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -229,6 +229,9 @@ func Test_RepoClone_hasParent(t *testing.T) {
|
|||
"name": "ORIG",
|
||||
"owner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"defaultBranchRef": {
|
||||
"name": "trunk"
|
||||
}
|
||||
}
|
||||
} } }
|
||||
|
|
@ -236,23 +239,21 @@ func Test_RepoClone_hasParent(t *testing.T) {
|
|||
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
cs, restore := test.InitCmdStubber()
|
||||
defer restore()
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
cs.Stub("") // git clone
|
||||
cs.Stub("") // git remote add
|
||||
cs.Register(`git clone https://github.com/OWNER/REPO.git`, 0, "")
|
||||
cs.Register(`git -C REPO remote add -t trunk -f upstream https://github.com/hubot/ORIG.git`, 0, "")
|
||||
|
||||
_, err := runCloneCommand(httpClient, "OWNER/REPO")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `repo clone`: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 2, cs.Count)
|
||||
assert.Equal(t, "git -C REPO remote add -f upstream https://github.com/hubot/ORIG.git", strings.Join(cs.Calls[1].Args, " "))
|
||||
}
|
||||
|
||||
func Test_RepoClone_withoutUsername(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`
|
||||
|
|
@ -269,19 +270,12 @@ func Test_RepoClone_withoutUsername(t *testing.T) {
|
|||
}
|
||||
} } }
|
||||
`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query RepositoryFindParent\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"parent": null
|
||||
} } }`))
|
||||
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
cs, restore := test.InitCmdStubber()
|
||||
defer restore()
|
||||
|
||||
cs.Stub("") // git clone
|
||||
cs, restore := run.Stub()
|
||||
defer restore(t)
|
||||
cs.Register(`git clone https://github\.com/OWNER/REPO\.git`, 0, "")
|
||||
|
||||
output, err := runCloneCommand(httpClient, "REPO")
|
||||
if err != nil {
|
||||
|
|
@ -290,6 +284,4 @@ func Test_RepoClone_withoutUsername(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
assert.Equal(t, 1, cs.Count)
|
||||
assert.Equal(t, "git clone https://github.com/OWNER/REPO.git", strings.Join(cs.Calls[0].Args, " "))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@ func createRun(opts *CreateOptions) error {
|
|||
|
||||
createLocalDirectory := opts.ConfirmSubmit
|
||||
if !opts.ConfirmSubmit {
|
||||
opts.ConfirmSubmit, err = confirmSubmission(input.Name, input.OwnerID, &opts.ConfirmSubmit)
|
||||
opts.ConfirmSubmit, err = confirmSubmission(input.Name, input.OwnerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -369,7 +369,7 @@ func interactiveRepoCreate(isDescEmpty bool, isVisibilityPassed bool, repoName s
|
|||
return answers.RepoName, answers.RepoDescription, strings.ToUpper(answers.RepoVisibility), nil
|
||||
}
|
||||
|
||||
func confirmSubmission(repoName string, repoOwner string, isConfirmFlagPassed *bool) (bool, error) {
|
||||
func confirmSubmission(repoName string, repoOwner string) (bool, error) {
|
||||
qs := []*survey.Question{}
|
||||
|
||||
promptString := ""
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ func TestRepoCreate(t *testing.T) {
|
|||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
var seenCmd *exec.Cmd
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmd = cmd
|
||||
return &test.OutputStub{}
|
||||
|
|
@ -170,6 +171,7 @@ func TestRepoCreate_outsideGitWorkDir(t *testing.T) {
|
|||
{},
|
||||
{},
|
||||
}
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
if len(cmdOutputs) == 0 {
|
||||
t.Fatal("Too many calls to git command")
|
||||
|
|
@ -244,6 +246,7 @@ func TestRepoCreate_org(t *testing.T) {
|
|||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
var seenCmd *exec.Cmd
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmd = cmd
|
||||
return &test.OutputStub{}
|
||||
|
|
@ -326,6 +329,7 @@ func TestRepoCreate_orgWithTeam(t *testing.T) {
|
|||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
var seenCmd *exec.Cmd
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmd = cmd
|
||||
return &test.OutputStub{}
|
||||
|
|
@ -409,6 +413,7 @@ func TestRepoCreate_template(t *testing.T) {
|
|||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
var seenCmd *exec.Cmd
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmd = cmd
|
||||
return &test.OutputStub{}
|
||||
|
|
@ -489,6 +494,7 @@ func TestRepoCreate_withoutNameArg(t *testing.T) {
|
|||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
var seenCmd *exec.Cmd
|
||||
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmd = cmd
|
||||
return &test.OutputStub{}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ import (
|
|||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/internal/run"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/pkg/prompt"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type ForkOptions struct {
|
||||
|
|
@ -27,11 +27,13 @@ type ForkOptions struct {
|
|||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Remotes func() (context.Remotes, error)
|
||||
|
||||
GitArgs []string
|
||||
Repository string
|
||||
Clone bool
|
||||
Remote bool
|
||||
PromptClone bool
|
||||
PromptRemote bool
|
||||
RemoteName string
|
||||
}
|
||||
|
||||
var Since = func(t time.Time) time.Duration {
|
||||
|
|
@ -48,16 +50,24 @@ func NewCmdFork(f *cmdutil.Factory, runF func(*ForkOptions) error) *cobra.Comman
|
|||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "fork [<repository>]",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Use: "fork [<repository>] [-- <gitflags>...]",
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if cmd.ArgsLenAtDash() == 0 && len(args[1:]) > 0 {
|
||||
return cmdutil.FlagError{Err: fmt.Errorf("repository argument required when passing 'git clone' flags")}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Short: "Create a fork of a repository",
|
||||
Long: `Create a fork of a repository.
|
||||
|
||||
With no argument, creates a fork of the current repository. Otherwise, forks the specified repository.`,
|
||||
With no argument, creates a fork of the current repository. Otherwise, forks the specified repository.
|
||||
|
||||
Additional 'git clone' flags can be passed in by listing them after '--'.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
promptOk := opts.IO.CanPrompt()
|
||||
if len(args) > 0 {
|
||||
opts.Repository = args[0]
|
||||
opts.GitArgs = args[1:]
|
||||
}
|
||||
|
||||
if promptOk && !cmd.Flags().Changed("clone") {
|
||||
|
|
@ -74,9 +84,16 @@ With no argument, creates a fork of the current repository. Otherwise, forks the
|
|||
return forkRun(opts)
|
||||
},
|
||||
}
|
||||
cmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
|
||||
if err == pflag.ErrHelp {
|
||||
return err
|
||||
}
|
||||
return &cmdutil.FlagError{Err: fmt.Errorf("%w\nSeparate git clone flags with '--'.", err)}
|
||||
})
|
||||
|
||||
cmd.Flags().BoolVar(&opts.Clone, "clone", false, "Clone the fork {true|false}")
|
||||
cmd.Flags().BoolVar(&opts.Remote, "remote", false, "Add remote for fork {true|false}")
|
||||
cmd.Flags().StringVar(&opts.RemoteName, "remote-name", "origin", "Specify a name for a fork's new remote.")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -127,18 +144,6 @@ func forkRun(opts *ForkOptions) error {
|
|||
|
||||
cs := opts.IO.ColorScheme()
|
||||
stderr := opts.IO.ErrOut
|
||||
s := utils.Spinner(stderr)
|
||||
stopSpinner := func() {}
|
||||
|
||||
if connectedToTerminal {
|
||||
loading := cs.Gray("Forking ") + cs.Bold(cs.Gray(ghrepo.FullName(repoToFork))) + cs.Gray("...")
|
||||
s.Suffix = " " + loading
|
||||
s.FinalMSG = cs.Gray(fmt.Sprintf("- %s\n", loading))
|
||||
utils.StartSpinner(s)
|
||||
stopSpinner = func() {
|
||||
utils.StopSpinner(s)
|
||||
}
|
||||
}
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
|
|
@ -147,14 +152,13 @@ func forkRun(opts *ForkOptions) error {
|
|||
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
opts.IO.StartProgressIndicator()
|
||||
forkedRepo, err := api.ForkRepo(apiClient, repoToFork)
|
||||
opts.IO.StopProgressIndicator()
|
||||
if err != nil {
|
||||
stopSpinner()
|
||||
return fmt.Errorf("failed to fork: %w", err)
|
||||
}
|
||||
|
||||
stopSpinner()
|
||||
|
||||
// This is weird. There is not an efficient way to determine via the GitHub API whether or not a
|
||||
// given user has forked a given repo. We noticed, also, that the create fork API endpoint just
|
||||
// returns the fork repo data even if it already exists -- with no change in status code or
|
||||
|
|
@ -225,25 +229,13 @@ func forkRun(opts *ForkOptions) error {
|
|||
}
|
||||
}
|
||||
if remoteDesired {
|
||||
remoteName := "origin"
|
||||
|
||||
remoteName := opts.RemoteName
|
||||
remotes, err := opts.Remotes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := remotes.FindByName(remoteName); err == nil {
|
||||
renameTarget := "upstream"
|
||||
renameCmd, err := git.GitCommand("remote", "rename", remoteName, renameTarget)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = run.PrepareCmd(renameCmd).Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if connectedToTerminal {
|
||||
fmt.Fprintf(stderr, "%s Renamed %s remote to %s\n", cs.SuccessIcon(), cs.Bold(remoteName), cs.Bold(renameTarget))
|
||||
}
|
||||
return fmt.Errorf("a remote called '%s' already exists. You can rerun this command with --remote-name to specify a different remote name.", remoteName)
|
||||
}
|
||||
|
||||
forkedRepoCloneURL := ghrepo.FormatRemoteURL(forkedRepo, protocol)
|
||||
|
|
@ -267,13 +259,13 @@ func forkRun(opts *ForkOptions) error {
|
|||
}
|
||||
if cloneDesired {
|
||||
forkedRepoURL := ghrepo.FormatRemoteURL(forkedRepo, protocol)
|
||||
cloneDir, err := git.RunClone(forkedRepoURL, []string{})
|
||||
cloneDir, err := git.RunClone(forkedRepoURL, opts.GitArgs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to clone fork: %w", err)
|
||||
}
|
||||
|
||||
upstreamURL := ghrepo.FormatRemoteURL(repoToFork, protocol)
|
||||
err = git.AddUpstreamRemote(upstreamURL, cloneDir)
|
||||
err = git.AddUpstreamRemote(upstreamURL, cloneDir, []string{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,10 @@ package fork
|
|||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/briandowns/spinner"
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/config"
|
||||
|
|
@ -20,7 +17,6 @@ import (
|
|||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/pkg/prompt"
|
||||
"github.com/cli/cli/test"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
|
@ -85,44 +81,85 @@ func runCommand(httpClient *http.Client, remotes []*context.Remote, isTTY bool,
|
|||
func TestRepoFork_nontty(t *testing.T) {
|
||||
defer stubSince(2 * time.Second)()
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
cs, restore := test.InitCmdStubber()
|
||||
defer restore()
|
||||
_, restore := run.Stub()
|
||||
defer restore(t)
|
||||
|
||||
output, err := runCommand(httpClient, nil, false, "")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 0, len(cs.Calls))
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
|
||||
}
|
||||
|
||||
func TestRepoFork_existing_remote_error(t *testing.T) {
|
||||
defer stubSince(2 * time.Second)()
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
_, err := runCommand(httpClient, nil, false, "--remote")
|
||||
if err == nil {
|
||||
t.Fatal("expected error running command `repo fork`")
|
||||
}
|
||||
|
||||
assert.Equal(t, "a remote called 'origin' already exists. You can rerun this command with --remote-name to specify a different remote name.", err.Error())
|
||||
|
||||
reg.Verify(t)
|
||||
}
|
||||
|
||||
func TestRepoFork_no_existing_remote(t *testing.T) {
|
||||
remotes := []*context.Remote{
|
||||
{
|
||||
Remote: &git.Remote{
|
||||
Name: "upstream",
|
||||
FetchURL: &url.URL{},
|
||||
},
|
||||
Repo: ghrepo.New("OWNER", "REPO"),
|
||||
},
|
||||
}
|
||||
defer stubSince(2 * time.Second)()
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
cs, restore := run.Stub()
|
||||
defer restore(t)
|
||||
|
||||
cs.Register(`git remote add -f origin https://github\.com/someone/REPO\.git`, 0, "")
|
||||
|
||||
output, err := runCommand(httpClient, remotes, false, "--remote")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
}
|
||||
|
||||
func TestRepoFork_in_parent_nontty(t *testing.T) {
|
||||
defer stubSince(2 * time.Second)()
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
cs, restore := test.InitCmdStubber()
|
||||
defer restore()
|
||||
cs, restore := run.Stub()
|
||||
defer restore(t)
|
||||
|
||||
cs.Stub("") // git remote rename
|
||||
cs.Stub("") // git remote add
|
||||
cs.Register(`git remote add -f fork https://github\.com/someone/REPO\.git`, 0, "")
|
||||
|
||||
output, err := runCommand(httpClient, nil, false, "--remote")
|
||||
output, err := runCommand(httpClient, nil, false, "--remote --remote-name=fork")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 2, len(cs.Calls))
|
||||
assert.Equal(t, "git remote rename origin upstream", strings.Join(cs.Calls[0].Args, " "))
|
||||
assert.Equal(t, "git remote add -f origin https://github.com/someone/REPO.git", strings.Join(cs.Calls[1].Args, " "))
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
reg.Verify(t)
|
||||
|
|
@ -131,14 +168,15 @@ func TestRepoFork_in_parent_nontty(t *testing.T) {
|
|||
func TestRepoFork_outside_parent_nontty(t *testing.T) {
|
||||
defer stubSince(2 * time.Second)()
|
||||
reg := &httpmock.Registry{}
|
||||
reg.Verify(t)
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
cs, restore := test.InitCmdStubber()
|
||||
defer restore()
|
||||
cs, restore := run.Stub()
|
||||
defer restore(t)
|
||||
|
||||
cs.Stub("") // git clone
|
||||
cs.Stub("") // git remote add
|
||||
cs.Register(`git clone https://github.com/someone/REPO\.git`, 0, "")
|
||||
cs.Register(`git -C REPO remote add -f upstream https://github\.com/OWNER/REPO\.git`, 0, "")
|
||||
|
||||
output, err := runCommand(httpClient, nil, false, "--clone OWNER/REPO")
|
||||
if err != nil {
|
||||
|
|
@ -146,30 +184,23 @@ func TestRepoFork_outside_parent_nontty(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
assert.Equal(t, "git clone https://github.com/someone/REPO.git", strings.Join(cs.Calls[0].Args, " "))
|
||||
assert.Equal(t, "git -C REPO remote add -f upstream https://github.com/OWNER/REPO.git", strings.Join(cs.Calls[1].Args, " "))
|
||||
|
||||
assert.Equal(t, output.Stderr(), "")
|
||||
reg.Verify(t)
|
||||
|
||||
}
|
||||
|
||||
func TestRepoFork_already_forked(t *testing.T) {
|
||||
stubSpinner()
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
cs, restore := test.InitCmdStubber()
|
||||
defer restore()
|
||||
_, restore := run.Stub()
|
||||
defer restore(t)
|
||||
|
||||
output, err := runCommand(httpClient, nil, true, "--remote=false")
|
||||
if err != nil {
|
||||
t.Errorf("got unexpected error: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 0, len(cs.Calls))
|
||||
|
||||
r := regexp.MustCompile(`someone/REPO.*already exists`)
|
||||
if !r.MatchString(output.Stderr()) {
|
||||
t.Errorf("output did not match regexp /%s/\n> output\n%s\n", r, output.Stderr())
|
||||
|
|
@ -180,7 +211,6 @@ func TestRepoFork_already_forked(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRepoFork_reuseRemote(t *testing.T) {
|
||||
stubSpinner()
|
||||
remotes := []*context.Remote{
|
||||
{
|
||||
Remote: &git.Remote{Name: "origin", FetchURL: &url.URL{}},
|
||||
|
|
@ -208,13 +238,12 @@ func TestRepoFork_reuseRemote(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRepoFork_in_parent(t *testing.T) {
|
||||
stubSpinner()
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
cs, restore := test.InitCmdStubber()
|
||||
defer restore()
|
||||
_, restore := run.Stub()
|
||||
defer restore(t)
|
||||
defer stubSince(2 * time.Second)()
|
||||
|
||||
output, err := runCommand(httpClient, nil, true, "--remote=false")
|
||||
|
|
@ -222,7 +251,6 @@ func TestRepoFork_in_parent(t *testing.T) {
|
|||
t.Errorf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 0, len(cs.Calls))
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
r := regexp.MustCompile(`Created fork.*someone/REPO`)
|
||||
|
|
@ -234,7 +262,6 @@ func TestRepoFork_in_parent(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRepoFork_outside(t *testing.T) {
|
||||
stubSpinner()
|
||||
tests := []struct {
|
||||
name string
|
||||
args string
|
||||
|
|
@ -273,52 +300,40 @@ func TestRepoFork_outside(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRepoFork_in_parent_yes(t *testing.T) {
|
||||
stubSpinner()
|
||||
defer stubSince(2 * time.Second)()
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
var seenCmds []*exec.Cmd
|
||||
defer run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmds = append(seenCmds, cmd)
|
||||
return &test.OutputStub{}
|
||||
})()
|
||||
cs, restore := run.Stub()
|
||||
defer restore(t)
|
||||
|
||||
output, err := runCommand(httpClient, nil, true, "--remote")
|
||||
cs.Register(`git remote add -f fork https://github\.com/someone/REPO\.git`, 0, "")
|
||||
|
||||
output, err := runCommand(httpClient, nil, true, "--remote --remote-name=fork")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
expectedCmds := []string{
|
||||
"git remote rename origin upstream",
|
||||
"git remote add -f origin https://github.com/someone/REPO.git",
|
||||
}
|
||||
|
||||
for x, cmd := range seenCmds {
|
||||
assert.Equal(t, expectedCmds[x], strings.Join(cmd.Args, " "))
|
||||
}
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(),
|
||||
"Created fork.*someone/REPO",
|
||||
"Added remote.*origin")
|
||||
"Added remote.*fork")
|
||||
reg.Verify(t)
|
||||
}
|
||||
|
||||
func TestRepoFork_outside_yes(t *testing.T) {
|
||||
stubSpinner()
|
||||
defer stubSince(2 * time.Second)()
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
cs, restore := test.InitCmdStubber()
|
||||
defer restore()
|
||||
cs, restore := run.Stub()
|
||||
defer restore(t)
|
||||
|
||||
cs.Stub("") // git clone
|
||||
cs.Stub("") // git remote add
|
||||
cs.Register(`git clone https://github\.com/someone/REPO\.git`, 0, "")
|
||||
cs.Register(`git -C REPO remote add -f upstream https://github\.com/OWNER/REPO\.git`, 0, "")
|
||||
|
||||
output, err := runCommand(httpClient, nil, true, "--clone OWNER/REPO")
|
||||
if err != nil {
|
||||
|
|
@ -326,10 +341,7 @@ func TestRepoFork_outside_yes(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
assert.Equal(t, "git clone https://github.com/someone/REPO.git", strings.Join(cs.Calls[0].Args, " "))
|
||||
assert.Equal(t, "git -C REPO remote add -f upstream https://github.com/OWNER/REPO.git", strings.Join(cs.Calls[1].Args, " "))
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(),
|
||||
"Created fork.*someone/REPO",
|
||||
"Cloned fork")
|
||||
|
|
@ -337,17 +349,16 @@ func TestRepoFork_outside_yes(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRepoFork_outside_survey_yes(t *testing.T) {
|
||||
stubSpinner()
|
||||
defer stubSince(2 * time.Second)()
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
cs, restore := test.InitCmdStubber()
|
||||
defer restore()
|
||||
cs, restore := run.Stub()
|
||||
defer restore(t)
|
||||
|
||||
cs.Stub("") // git clone
|
||||
cs.Stub("") // git remote add
|
||||
cs.Register(`git clone https://github\.com/someone/REPO\.git`, 0, "")
|
||||
cs.Register(`git -C REPO remote add -f upstream https://github\.com/OWNER/REPO\.git`, 0, "")
|
||||
|
||||
defer prompt.StubConfirm(true)()
|
||||
|
||||
|
|
@ -357,10 +368,7 @@ func TestRepoFork_outside_survey_yes(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
assert.Equal(t, "git clone https://github.com/someone/REPO.git", strings.Join(cs.Calls[0].Args, " "))
|
||||
assert.Equal(t, "git -C REPO remote add -f upstream https://github.com/OWNER/REPO.git", strings.Join(cs.Calls[1].Args, " "))
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(),
|
||||
"Created fork.*someone/REPO",
|
||||
"Cloned fork")
|
||||
|
|
@ -368,17 +376,13 @@ func TestRepoFork_outside_survey_yes(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRepoFork_outside_survey_no(t *testing.T) {
|
||||
stubSpinner()
|
||||
defer stubSince(2 * time.Second)()
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
cmdRun := false
|
||||
defer run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
cmdRun = true
|
||||
return &test.OutputStub{}
|
||||
})()
|
||||
_, restore := run.Stub()
|
||||
defer restore(t)
|
||||
|
||||
defer prompt.StubConfirm(false)()
|
||||
|
||||
|
|
@ -389,8 +393,6 @@ func TestRepoFork_outside_survey_no(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
assert.Equal(t, false, cmdRun)
|
||||
|
||||
r := regexp.MustCompile(`Created fork.*someone/REPO`)
|
||||
if !r.MatchString(output.Stderr()) {
|
||||
t.Errorf("output did not match regexp /%s/\n> output\n%s\n", r, output)
|
||||
|
|
@ -400,55 +402,41 @@ func TestRepoFork_outside_survey_no(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRepoFork_in_parent_survey_yes(t *testing.T) {
|
||||
stubSpinner()
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
defer stubSince(2 * time.Second)()
|
||||
|
||||
var seenCmds []*exec.Cmd
|
||||
defer run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmds = append(seenCmds, cmd)
|
||||
return &test.OutputStub{}
|
||||
})()
|
||||
cs, restore := run.Stub()
|
||||
defer restore(t)
|
||||
|
||||
cs.Register(`git remote add -f fork https://github\.com/someone/REPO\.git`, 0, "")
|
||||
|
||||
defer prompt.StubConfirm(true)()
|
||||
|
||||
output, err := runCommand(httpClient, nil, true, "")
|
||||
output, err := runCommand(httpClient, nil, true, "--remote-name=fork")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
expectedCmds := []string{
|
||||
"git remote rename origin upstream",
|
||||
"git remote add -f origin https://github.com/someone/REPO.git",
|
||||
}
|
||||
|
||||
for x, cmd := range seenCmds {
|
||||
assert.Equal(t, expectedCmds[x], strings.Join(cmd.Args, " "))
|
||||
}
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(),
|
||||
"Created fork.*someone/REPO",
|
||||
"Renamed.*origin.*remote to.*upstream",
|
||||
"Added remote.*origin")
|
||||
"Added remote.*fork")
|
||||
reg.Verify(t)
|
||||
}
|
||||
|
||||
func TestRepoFork_in_parent_survey_no(t *testing.T) {
|
||||
stubSpinner()
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
defer stubSince(2 * time.Second)()
|
||||
|
||||
cmdRun := false
|
||||
defer run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
cmdRun = true
|
||||
return &test.OutputStub{}
|
||||
})()
|
||||
_, restore := run.Stub()
|
||||
defer restore(t)
|
||||
|
||||
defer prompt.StubConfirm(false)()
|
||||
|
||||
|
|
@ -459,28 +447,53 @@ func TestRepoFork_in_parent_survey_no(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
assert.Equal(t, false, cmdRun)
|
||||
|
||||
r := regexp.MustCompile(`Created fork.*someone/REPO`)
|
||||
if !r.MatchString(output.Stderr()) {
|
||||
t.Errorf("output did not match regexp /%s/\n> output\n%s\n", r, output)
|
||||
return
|
||||
}
|
||||
reg.Verify(t)
|
||||
}
|
||||
|
||||
func TestRepoFork_in_parent_match_protocol(t *testing.T) {
|
||||
stubSpinner()
|
||||
func Test_RepoFork_gitFlags(t *testing.T) {
|
||||
defer stubSince(2 * time.Second)()
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
var seenCmds []*exec.Cmd
|
||||
defer run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmds = append(seenCmds, cmd)
|
||||
return &test.OutputStub{}
|
||||
})()
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
cs.Register(`git clone --depth 1 https://github.com/someone/REPO.git`, 0, "")
|
||||
cs.Register(`git -C REPO remote add -f upstream https://github.com/OWNER/REPO.git`, 0, "")
|
||||
|
||||
output, err := runCommand(httpClient, nil, false, "--clone OWNER/REPO -- --depth 1")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, output.Stderr(), "")
|
||||
reg.Verify(t)
|
||||
}
|
||||
|
||||
func Test_RepoFork_flagError(t *testing.T) {
|
||||
_, err := runCommand(nil, nil, true, "--depth 1 OWNER/REPO")
|
||||
if err == nil || err.Error() != "unknown flag: --depth\nSeparate git clone flags with '--'." {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoFork_in_parent_match_protocol(t *testing.T) {
|
||||
defer stubSince(2 * time.Second)()
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
cs, restore := run.Stub()
|
||||
defer restore(t)
|
||||
|
||||
cs.Register(`git remote add -f fork git@github\.com:someone/REPO\.git`, 0, "")
|
||||
|
||||
remotes := []*context.Remote{
|
||||
{
|
||||
|
|
@ -491,34 +504,17 @@ func TestRepoFork_in_parent_match_protocol(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
output, err := runCommand(httpClient, remotes, true, "--remote")
|
||||
output, err := runCommand(httpClient, remotes, true, "--remote --remote-name=fork")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
expectedCmds := []string{
|
||||
"git remote rename origin upstream",
|
||||
"git remote add -f origin git@github.com:someone/REPO.git",
|
||||
}
|
||||
|
||||
for x, cmd := range seenCmds {
|
||||
assert.Equal(t, expectedCmds[x], strings.Join(cmd.Args, " "))
|
||||
}
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(),
|
||||
"Created fork.*someone/REPO",
|
||||
"Added remote.*origin")
|
||||
reg.Verify(t)
|
||||
}
|
||||
|
||||
func stubSpinner() {
|
||||
// not bothering with teardown since we never want spinners when doing tests
|
||||
utils.StartSpinner = func(_ *spinner.Spinner) {
|
||||
}
|
||||
utils.StopSpinner = func(_ *spinner.Spinner) {
|
||||
}
|
||||
"Added remote.*fork")
|
||||
}
|
||||
|
||||
func stubSince(d time.Duration) func() {
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ import (
|
|||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/internal/run"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/test"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
|
@ -132,18 +132,14 @@ func Test_RepoView_Web(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
io.SetStdoutTTY(tt.stdoutTTY)
|
||||
|
||||
cs, teardown := test.InitCmdStubber()
|
||||
defer teardown()
|
||||
|
||||
cs.Stub("") // browser open
|
||||
cs, teardown := run.Stub()
|
||||
defer teardown(t)
|
||||
cs.Register(`https://github\.com/OWNER/REPO$`, 0, "")
|
||||
|
||||
if err := viewRun(opts); err != nil {
|
||||
t.Errorf("viewRun() error = %v", err)
|
||||
}
|
||||
assert.Equal(t, "", stdout.String())
|
||||
assert.Equal(t, 1, len(cs.Calls))
|
||||
call := cs.Calls[0]
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO", call.Args[len(call.Args)-1])
|
||||
assert.Equal(t, tt.wantStderr, stderr.String())
|
||||
reg.Verify(t)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import (
|
|||
repoCmd "github.com/cli/cli/pkg/cmd/repo"
|
||||
creditsCmd "github.com/cli/cli/pkg/cmd/repo/credits"
|
||||
secretCmd "github.com/cli/cli/pkg/cmd/secret"
|
||||
sshKeyCmd "github.com/cli/cli/pkg/cmd/ssh-key"
|
||||
versionCmd "github.com/cli/cli/pkg/cmd/version"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -76,6 +77,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command {
|
|||
cmd.AddCommand(gistCmd.NewCmdGist(f))
|
||||
cmd.AddCommand(completionCmd.NewCmdCompletion(f.IOStreams))
|
||||
cmd.AddCommand(secretCmd.NewCmdSecret(f))
|
||||
cmd.AddCommand(sshKeyCmd.NewCmdSSHKey(f))
|
||||
|
||||
// the `api` command should not inherit any extra HTTP headers
|
||||
bareHTTPCmdFactory := *f
|
||||
|
|
|
|||
|
|
@ -191,6 +191,7 @@ func Test_listRun(t *testing.T) {
|
|||
|
||||
reg.Verify(t)
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, stdout.String(), tt.wantOut...)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
58
pkg/cmd/ssh-key/list/http.go
Normal file
58
pkg/cmd/ssh-key/list/http.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/ghinstance"
|
||||
)
|
||||
|
||||
var scopesError = errors.New("insufficient OAuth scopes")
|
||||
|
||||
type sshKey struct {
|
||||
Key string
|
||||
Title string
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func userKeys(httpClient *http.Client, userHandle string) ([]sshKey, error) {
|
||||
resource := "user/keys"
|
||||
if userHandle != "" {
|
||||
resource = fmt.Sprintf("users/%s/keys", userHandle)
|
||||
}
|
||||
url := fmt.Sprintf("%s%s?per_page=%d", ghinstance.RESTPrefix(ghinstance.OverridableDefault()), resource, 100)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return nil, scopesError
|
||||
} else if resp.StatusCode > 299 {
|
||||
return nil, api.HandleHTTPError(resp)
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var keys []sshKey
|
||||
err = json.Unmarshal(b, &keys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return keys, nil
|
||||
}
|
||||
96
pkg/cmd/ssh-key/list/list.go
Normal file
96
pkg/cmd/ssh-key/list/list.go
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ListOptions struct for list command
|
||||
type ListOptions struct {
|
||||
IO *iostreams.IOStreams
|
||||
HTTPClient func() (*http.Client, error)
|
||||
}
|
||||
|
||||
// NewCmdList creates a command for list all SSH Keys
|
||||
func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command {
|
||||
opts := &ListOptions{
|
||||
HTTPClient: f.HttpClient,
|
||||
IO: f.IOStreams,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "Lists SSH keys in a GitHub account",
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
return listRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func listRun(opts *ListOptions) error {
|
||||
apiClient, err := opts.HTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sshKeys, err := userKeys(apiClient, "")
|
||||
if err != nil {
|
||||
if errors.Is(err, scopesError) {
|
||||
cs := opts.IO.ColorScheme()
|
||||
fmt.Fprint(opts.IO.ErrOut, "Error: insufficient OAuth scopes to list SSH keys\n")
|
||||
fmt.Fprintf(opts.IO.ErrOut, "Run the following to grant scopes: %s\n", cs.Bold("gh auth refresh -s read:public_key"))
|
||||
return cmdutil.SilentError
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if len(sshKeys) == 0 {
|
||||
fmt.Fprintln(opts.IO.ErrOut, "No SSH keys present in GitHub account.")
|
||||
return cmdutil.SilentError
|
||||
}
|
||||
|
||||
t := utils.NewTablePrinter(opts.IO)
|
||||
cs := opts.IO.ColorScheme()
|
||||
now := time.Now()
|
||||
|
||||
for _, sshKey := range sshKeys {
|
||||
t.AddField(sshKey.Title, nil, nil)
|
||||
t.AddField(sshKey.Key, truncateMiddle, nil)
|
||||
|
||||
createdAt := sshKey.CreatedAt.Format(time.RFC3339)
|
||||
if t.IsTTY() {
|
||||
createdAt = utils.FuzzyAgoAbbr(now, sshKey.CreatedAt)
|
||||
}
|
||||
t.AddField(createdAt, nil, cs.Gray)
|
||||
t.EndRow()
|
||||
}
|
||||
|
||||
return t.Render()
|
||||
}
|
||||
|
||||
func truncateMiddle(maxWidth int, t string) string {
|
||||
if len(t) <= maxWidth {
|
||||
return t
|
||||
}
|
||||
|
||||
ellipsis := "..."
|
||||
if maxWidth < len(ellipsis)+2 {
|
||||
return t[0:maxWidth]
|
||||
}
|
||||
|
||||
halfWidth := (maxWidth - len(ellipsis)) / 2
|
||||
return t[0:halfWidth] + ellipsis + t[len(t)-halfWidth:]
|
||||
}
|
||||
131
pkg/cmd/ssh-key/list/list_test.go
Normal file
131
pkg/cmd/ssh-key/list/list_test.go
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
)
|
||||
|
||||
func TestListRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts ListOptions
|
||||
isTTY bool
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "list tty",
|
||||
opts: ListOptions{
|
||||
HTTPClient: func() (*http.Client, error) {
|
||||
createdAt := time.Now().Add(time.Duration(-24) * time.Hour)
|
||||
reg := &httpmock.Registry{}
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "user/keys"),
|
||||
httpmock.StringResponse(fmt.Sprintf(`[
|
||||
{
|
||||
"id": 1234,
|
||||
"key": "ssh-rsa AAAABbBB123",
|
||||
"title": "Mac",
|
||||
"created_at": "%[1]s"
|
||||
},
|
||||
{
|
||||
"id": 5678,
|
||||
"key": "ssh-rsa EEEEEEEK247",
|
||||
"title": "hubot@Windows",
|
||||
"created_at": "%[1]s"
|
||||
}
|
||||
]`, createdAt.Format(time.RFC3339))),
|
||||
)
|
||||
return &http.Client{Transport: reg}, nil
|
||||
},
|
||||
},
|
||||
isTTY: true,
|
||||
wantStdout: heredoc.Doc(`
|
||||
Mac ssh-rsa AAAABbBB123 1d
|
||||
hubot@Windows ssh-rsa EEEEEEEK247 1d
|
||||
`),
|
||||
wantStderr: "",
|
||||
},
|
||||
{
|
||||
name: "list non-tty",
|
||||
opts: ListOptions{
|
||||
HTTPClient: func() (*http.Client, error) {
|
||||
createdAt, _ := time.Parse(time.RFC3339, "2020-08-31T15:44:24+02:00")
|
||||
reg := &httpmock.Registry{}
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "user/keys"),
|
||||
httpmock.StringResponse(fmt.Sprintf(`[
|
||||
{
|
||||
"id": 1234,
|
||||
"key": "ssh-rsa AAAABbBB123",
|
||||
"title": "Mac",
|
||||
"created_at": "%[1]s"
|
||||
},
|
||||
{
|
||||
"id": 5678,
|
||||
"key": "ssh-rsa EEEEEEEK247",
|
||||
"title": "hubot@Windows",
|
||||
"created_at": "%[1]s"
|
||||
}
|
||||
]`, createdAt.Format(time.RFC3339))),
|
||||
)
|
||||
return &http.Client{Transport: reg}, nil
|
||||
},
|
||||
},
|
||||
isTTY: false,
|
||||
wantStdout: heredoc.Doc(`
|
||||
Mac ssh-rsa AAAABbBB123 2020-08-31T15:44:24+02:00
|
||||
hubot@Windows ssh-rsa EEEEEEEK247 2020-08-31T15:44:24+02:00
|
||||
`),
|
||||
wantStderr: "",
|
||||
},
|
||||
{
|
||||
name: "no keys",
|
||||
opts: ListOptions{
|
||||
HTTPClient: func() (*http.Client, error) {
|
||||
reg := &httpmock.Registry{}
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "user/keys"),
|
||||
httpmock.StringResponse(`[]`),
|
||||
)
|
||||
return &http.Client{Transport: reg}, nil
|
||||
},
|
||||
},
|
||||
wantStdout: "",
|
||||
wantStderr: "No SSH keys present in GitHub account.\n",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
io, _, stdout, stderr := iostreams.Test()
|
||||
io.SetStdoutTTY(tt.isTTY)
|
||||
io.SetStdinTTY(tt.isTTY)
|
||||
io.SetStderrTTY(tt.isTTY)
|
||||
|
||||
opts := tt.opts
|
||||
opts.IO = io
|
||||
|
||||
err := listRun(&opts)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("linRun() return error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if stdout.String() != tt.wantStdout {
|
||||
t.Errorf("wants stdout %q, got %q", tt.wantStdout, stdout.String())
|
||||
}
|
||||
if stderr.String() != tt.wantStderr {
|
||||
t.Errorf("wants stderr %q, got %q", tt.wantStderr, stderr.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
22
pkg/cmd/ssh-key/ssh-key.go
Normal file
22
pkg/cmd/ssh-key/ssh-key.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package key
|
||||
|
||||
import (
|
||||
cmdList "github.com/cli/cli/pkg/cmd/ssh-key/list"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewCmdSSHKey creates a command for manage SSH Keys
|
||||
func NewCmdSSHKey(f *cmdutil.Factory) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "ssh-key <command>",
|
||||
Short: "Manage SSH keys",
|
||||
Long: "Work with GitHub SSH keys",
|
||||
|
||||
Hidden: true,
|
||||
}
|
||||
|
||||
cmd.AddCommand(cmdList.NewCmdList(f, nil))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -157,3 +157,7 @@ func (e *GhEditor) Prompt(config *survey.PromptConfig) (interface{}, error) {
|
|||
}
|
||||
return e.prompt(initialValue, config)
|
||||
}
|
||||
|
||||
func DefaultEditorName() string {
|
||||
return filepath.Base(defaultEditor)
|
||||
}
|
||||
|
|
|
|||
196
script/build.go
Normal file
196
script/build.go
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
// Build tasks for the GitHub CLI project.
|
||||
//
|
||||
// Usage: go run script/build.go [<task>]
|
||||
//
|
||||
// Known tasks are:
|
||||
//
|
||||
// bin/gh:
|
||||
// Builds the main executable.
|
||||
// Supported environment variables:
|
||||
// - GH_VERSION: determined from source by default
|
||||
// - GH_OAUTH_CLIENT_ID
|
||||
// - GH_OAUTH_CLIENT_SECRET
|
||||
// - SOURCE_DATE_EPOCH: enables reproducible builds
|
||||
// - GO_LDFLAGS
|
||||
//
|
||||
// manpages:
|
||||
// Builds the man pages under `share/man/man1/`.
|
||||
//
|
||||
// clean:
|
||||
// Deletes all built files.
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cli/safeexec"
|
||||
)
|
||||
|
||||
var tasks = map[string]func(string) error{
|
||||
"bin/gh": func(exe string) error {
|
||||
info, err := os.Stat(exe)
|
||||
if err == nil && !sourceFilesLaterThan(info.ModTime()) {
|
||||
fmt.Printf("%s: `%s` is up to date.\n", self, exe)
|
||||
return nil
|
||||
}
|
||||
|
||||
ldflags := os.Getenv("GO_LDFLAGS")
|
||||
ldflags = fmt.Sprintf("-X github.com/cli/cli/internal/build.Version=%s %s", version(), ldflags)
|
||||
ldflags = fmt.Sprintf("-X github.com/cli/cli/internal/build.Date=%s %s", date(), ldflags)
|
||||
if oauthSecret := os.Getenv("GH_OAUTH_CLIENT_SECRET"); oauthSecret != "" {
|
||||
ldflags = fmt.Sprintf("-X github.com/cli/cli/internal/authflow.oauthClientSecret=%s %s", oauthSecret, ldflags)
|
||||
ldflags = fmt.Sprintf("-X github.com/cli/cli/internal/authflow.oauthClientID=%s %s", os.Getenv("GH_OAUTH_CLIENT_ID"), ldflags)
|
||||
}
|
||||
|
||||
return run("go", "build", "-trimpath", "-ldflags", ldflags, "-o", exe, "./cmd/gh")
|
||||
},
|
||||
"manpages": func(_ string) error {
|
||||
return run("go", "run", "./cmd/gen-docs", "--man-page", "--doc-path", "./share/man/man1/")
|
||||
},
|
||||
"clean": func(_ string) error {
|
||||
return rmrf("bin", "share")
|
||||
},
|
||||
}
|
||||
|
||||
var self string
|
||||
|
||||
func main() {
|
||||
task := "bin/gh"
|
||||
if runtime.GOOS == "windows" {
|
||||
task = "bin\\gh.exe"
|
||||
}
|
||||
|
||||
if len(os.Args) > 1 {
|
||||
task = os.Args[1]
|
||||
}
|
||||
|
||||
self = filepath.Base(os.Args[0])
|
||||
if self == "build" {
|
||||
self = "build.go"
|
||||
}
|
||||
|
||||
t := tasks[normalizeTask(task)]
|
||||
if t == nil {
|
||||
fmt.Fprintf(os.Stderr, "Don't know how to build task `%s`.\n", task)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err := t(task)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
fmt.Fprintf(os.Stderr, "%s: building task `%s` failed.\n", self, task)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func version() string {
|
||||
if versionEnv := os.Getenv("GH_VERSION"); versionEnv != "" {
|
||||
return versionEnv
|
||||
}
|
||||
if desc, err := cmdOutput("git", "describe", "--tags"); err == nil {
|
||||
return desc
|
||||
}
|
||||
rev, _ := cmdOutput("git", "rev-parse", "--short", "HEAD")
|
||||
return rev
|
||||
}
|
||||
|
||||
func date() string {
|
||||
t := time.Now()
|
||||
if sourceDate := os.Getenv("SOURCE_DATE_EPOCH"); sourceDate != "" {
|
||||
if sec, err := strconv.ParseInt(sourceDate, 10, 64); err == nil {
|
||||
t = time.Unix(sec, 0)
|
||||
}
|
||||
}
|
||||
return t.Format("2006-01-02")
|
||||
}
|
||||
|
||||
func sourceFilesLaterThan(t time.Time) bool {
|
||||
foundLater := false
|
||||
_ = filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if foundLater {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if len(path) > 1 && (path[0] == '.' || path[0] == '_') {
|
||||
if info.IsDir() {
|
||||
return filepath.SkipDir
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if path == "go.mod" || path == "go.sum" || (strings.HasSuffix(path, ".go") && !strings.HasSuffix(path, "_test.go")) {
|
||||
if info.ModTime().After(t) {
|
||||
foundLater = true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return foundLater
|
||||
}
|
||||
|
||||
func rmrf(targets ...string) error {
|
||||
args := append([]string{"rm", "-rf"}, targets...)
|
||||
announce(args...)
|
||||
for _, target := range targets {
|
||||
if err := os.RemoveAll(target); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func announce(args ...string) {
|
||||
fmt.Println(shellInspect(args))
|
||||
}
|
||||
|
||||
func run(args ...string) error {
|
||||
exe, err := safeexec.LookPath(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
announce(args...)
|
||||
cmd := exec.Command(exe, args[1:]...)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func cmdOutput(args ...string) (string, error) {
|
||||
exe, err := safeexec.LookPath(args[0])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
cmd := exec.Command(exe, args[1:]...)
|
||||
cmd.Stderr = ioutil.Discard
|
||||
out, err := cmd.Output()
|
||||
return strings.TrimSuffix(string(out), "\n"), err
|
||||
}
|
||||
|
||||
func shellInspect(args []string) string {
|
||||
fmtArgs := make([]string, len(args))
|
||||
for i, arg := range args {
|
||||
if strings.ContainsAny(arg, " \t'\"") {
|
||||
fmtArgs[i] = fmt.Sprintf("%q", arg)
|
||||
} else {
|
||||
fmtArgs[i] = arg
|
||||
}
|
||||
}
|
||||
return strings.Join(fmtArgs, " ")
|
||||
}
|
||||
|
||||
func normalizeTask(t string) string {
|
||||
return filepath.ToSlash(strings.TrimSuffix(t, ".exe"))
|
||||
}
|
||||
1
test/fixtures/test.git/HEAD
vendored
1
test/fixtures/test.git/HEAD
vendored
|
|
@ -1 +0,0 @@
|
|||
ref: refs/heads/master
|
||||
6
test/fixtures/test.git/config
vendored
6
test/fixtures/test.git/config
vendored
|
|
@ -1,6 +0,0 @@
|
|||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = true
|
||||
ignorecase = true
|
||||
precomposeunicode = false
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue