Merge branch 'trunk' of https://github.com/cli/cli into feature/action-headers
This commit is contained in:
commit
6b0a07f22e
99 changed files with 3279 additions and 3276 deletions
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
|
|
@ -36,6 +36,8 @@ Run the new binary as:
|
|||
|
||||
Run tests with: `go test ./...`
|
||||
|
||||
See [project layout documentation](../project-layout.md) for information on where to find specific source files.
|
||||
|
||||
## Submitting a pull request
|
||||
|
||||
1. Create a new branch: `git checkout -b my-branch-name`
|
||||
|
|
|
|||
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<!--
|
||||
Thank you for contributing to GitHub CLI!
|
||||
To reference an open issue, please write this in your description: `Fixes #NUMBER`
|
||||
-->
|
||||
19
.github/PULL_REQUEST_TEMPLATE/bug_fix.md
vendored
19
.github/PULL_REQUEST_TEMPLATE/bug_fix.md
vendored
|
|
@ -1,19 +0,0 @@
|
|||
---
|
||||
name: "\U0001F41B Bug fix"
|
||||
about: Fix a bug in GitHub CLI
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Please make sure you read our contributing guidelines at
|
||||
https://github.com/cli/cli/blob/trunk/.github/CONTRIBUTING.md
|
||||
before opening a pull request. Thanks!
|
||||
-->
|
||||
|
||||
## Summary
|
||||
|
||||
closes #[issue number]
|
||||
|
||||
## Details
|
||||
|
||||
-
|
||||
24
.github/workflows/releases.yml
vendored
24
.github/workflows/releases.yml
vendored
|
|
@ -80,7 +80,7 @@ jobs:
|
|||
popd
|
||||
- name: Run reprepro
|
||||
env:
|
||||
RELEASES: "cosmic eoan disco groovy focal stable oldstable testing unstable buster bullseye stretch jessie bionic trusty precise xenial"
|
||||
RELEASES: "cosmic eoan disco groovy focal stable oldstable testing unstable buster bullseye stretch jessie bionic trusty precise xenial hirsute impish kali-rolling"
|
||||
run: |
|
||||
mkdir -p upload
|
||||
for release in $RELEASES; do
|
||||
|
|
@ -133,9 +133,11 @@ jobs:
|
|||
- name: Build MSI
|
||||
id: buildmsi
|
||||
shell: bash
|
||||
env:
|
||||
ZIP_FILE: ${{ steps.download_exe.outputs.zip }}
|
||||
run: |
|
||||
mkdir -p build
|
||||
msi="$(basename "${{ steps.download_exe.outputs.zip }}" ".zip").msi"
|
||||
msi="$(basename "$ZIP_FILE" ".zip").msi"
|
||||
printf "::set-output name=msi::%s\n" "$msi"
|
||||
go-msi make --msi "$PWD/$msi" --out "$PWD/build" --version "${GITHUB_REF#refs/tags/}"
|
||||
- name: Obtain signing cert
|
||||
|
|
@ -145,14 +147,24 @@ jobs:
|
|||
run: .\script\setup-windows-certificate.ps1
|
||||
- name: Sign MSI
|
||||
env:
|
||||
CERT_FILE: ${{ steps.obtain_cert.outputs.cert-file }}
|
||||
EXE_FILE: ${{ steps.buildmsi.outputs.msi }}
|
||||
GITHUB_CERT_PASSWORD: ${{ secrets.GITHUB_CERT_PASSWORD }}
|
||||
run: |
|
||||
.\script\sign.ps1 -Certificate "${{ steps.obtain_cert.outputs.cert-file }}" `
|
||||
-Executable "${{ steps.buildmsi.outputs.msi }}"
|
||||
run: .\script\sign.ps1 -Certificate $env:CERT_FILE -Executable $env:EXE_FILE
|
||||
- name: Upload MSI
|
||||
shell: bash
|
||||
run: hub release edit "${GITHUB_REF#refs/tags/}" -m "" --draft=false -a "${{ steps.buildmsi.outputs.msi }}"
|
||||
run: |
|
||||
tag_name="${GITHUB_REF#refs/tags/}"
|
||||
hub release edit "$tag_name" -m "" -a "$MSI_FILE"
|
||||
release_url="$(gh api repos/:owner/:repo/releases -q ".[]|select(.tag_name==\"${tag_name}\")|.url")"
|
||||
publish_args=( -F draft=false )
|
||||
if [[ $GITHUB_REF != *-* ]]; then
|
||||
publish_args+=( -f discussion_category_name="$DISCUSSION_CATEGORY" )
|
||||
fi
|
||||
gh api -X PATCH "$release_url" "${publish_args[@]}"
|
||||
env:
|
||||
MSI_FILE: ${{ steps.buildmsi.outputs.msi }}
|
||||
DISCUSSION_CATEGORY: General
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
- name: Bump homebrew-core formula
|
||||
uses: mislav/bump-homebrew-formula-action@v1
|
||||
|
|
|
|||
|
|
@ -6,16 +6,11 @@ import (
|
|||
)
|
||||
|
||||
func (issue *Issue) ExportData(fields []string) *map[string]interface{} {
|
||||
v := reflect.ValueOf(issue).Elem()
|
||||
data := map[string]interface{}{}
|
||||
|
||||
for _, f := range fields {
|
||||
switch f {
|
||||
case "milestone":
|
||||
if issue.Milestone.Title != "" {
|
||||
data[f] = &issue.Milestone
|
||||
} else {
|
||||
data[f] = nil
|
||||
}
|
||||
case "comments":
|
||||
data[f] = issue.Comments.Nodes
|
||||
case "assignees":
|
||||
|
|
@ -25,7 +20,6 @@ func (issue *Issue) ExportData(fields []string) *map[string]interface{} {
|
|||
case "projectCards":
|
||||
data[f] = issue.ProjectCards.Nodes
|
||||
default:
|
||||
v := reflect.ValueOf(issue).Elem()
|
||||
sf := fieldByName(v, f)
|
||||
data[f] = sf.Interface()
|
||||
}
|
||||
|
|
@ -35,24 +29,42 @@ func (issue *Issue) ExportData(fields []string) *map[string]interface{} {
|
|||
}
|
||||
|
||||
func (pr *PullRequest) ExportData(fields []string) *map[string]interface{} {
|
||||
v := reflect.ValueOf(pr).Elem()
|
||||
data := map[string]interface{}{}
|
||||
|
||||
for _, f := range fields {
|
||||
switch f {
|
||||
case "headRepository":
|
||||
data[f] = map[string]string{"name": pr.HeadRepository.Name}
|
||||
case "milestone":
|
||||
if pr.Milestone.Title != "" {
|
||||
data[f] = &pr.Milestone
|
||||
} else {
|
||||
data[f] = nil
|
||||
}
|
||||
data[f] = pr.HeadRepository
|
||||
case "statusCheckRollup":
|
||||
if n := pr.Commits.Nodes; len(n) > 0 {
|
||||
if n := pr.StatusCheckRollup.Nodes; len(n) > 0 {
|
||||
data[f] = n[0].Commit.StatusCheckRollup.Contexts.Nodes
|
||||
} else {
|
||||
data[f] = nil
|
||||
}
|
||||
case "commits":
|
||||
commits := make([]interface{}, 0, len(pr.Commits.Nodes))
|
||||
for _, c := range pr.Commits.Nodes {
|
||||
commit := c.Commit
|
||||
authors := make([]interface{}, 0, len(commit.Authors.Nodes))
|
||||
for _, author := range commit.Authors.Nodes {
|
||||
authors = append(authors, map[string]interface{}{
|
||||
"name": author.Name,
|
||||
"email": author.Email,
|
||||
"id": author.User.ID,
|
||||
"login": author.User.Login,
|
||||
})
|
||||
}
|
||||
commits = append(commits, map[string]interface{}{
|
||||
"oid": commit.OID,
|
||||
"messageHeadline": commit.MessageHeadline,
|
||||
"messageBody": commit.MessageBody,
|
||||
"committedDate": commit.CommittedDate,
|
||||
"authoredDate": commit.AuthoredDate,
|
||||
"authors": authors,
|
||||
})
|
||||
}
|
||||
data[f] = commits
|
||||
case "comments":
|
||||
data[f] = pr.Comments.Nodes
|
||||
case "assignees":
|
||||
|
|
@ -75,7 +87,6 @@ func (pr *PullRequest) ExportData(fields []string) *map[string]interface{} {
|
|||
}
|
||||
data[f] = &requests
|
||||
default:
|
||||
v := reflect.ValueOf(pr).Elem()
|
||||
sf := fieldByName(v, f)
|
||||
data[f] = sf.Interface()
|
||||
}
|
||||
|
|
@ -84,22 +95,6 @@ func (pr *PullRequest) ExportData(fields []string) *map[string]interface{} {
|
|||
return &data
|
||||
}
|
||||
|
||||
func ExportIssues(issues []Issue, fields []string) *[]interface{} {
|
||||
data := make([]interface{}, len(issues))
|
||||
for i := range issues {
|
||||
data[i] = issues[i].ExportData(fields)
|
||||
}
|
||||
return &data
|
||||
}
|
||||
|
||||
func ExportPRs(prs []PullRequest, fields []string) *[]interface{} {
|
||||
data := make([]interface{}, len(prs))
|
||||
for i := range prs {
|
||||
data[i] = prs[i].ExportData(fields)
|
||||
}
|
||||
return &data
|
||||
}
|
||||
|
||||
func fieldByName(v reflect.Value, field string) reflect.Value {
|
||||
return v.FieldByNameFunc(func(s string) bool {
|
||||
return strings.EqualFold(field, s)
|
||||
|
|
|
|||
|
|
@ -40,7 +40,10 @@ func TestIssue_ExportData(t *testing.T) {
|
|||
outputJSON: heredoc.Doc(`
|
||||
{
|
||||
"milestone": {
|
||||
"title": "The next big thing"
|
||||
"number": 0,
|
||||
"title": "The next big thing",
|
||||
"description": "",
|
||||
"dueOn": null
|
||||
},
|
||||
"number": 2345
|
||||
}
|
||||
|
|
@ -90,31 +93,6 @@ func TestIssue_ExportData(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestExportIssues(t *testing.T) {
|
||||
issues := []Issue{
|
||||
{Milestone: Milestone{Title: "hi"}},
|
||||
{},
|
||||
}
|
||||
exported := ExportIssues(issues, []string{"milestone"})
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
enc := json.NewEncoder(&buf)
|
||||
enc.SetIndent("", "\t")
|
||||
require.NoError(t, enc.Encode(exported))
|
||||
assert.Equal(t, heredoc.Doc(`
|
||||
[
|
||||
{
|
||||
"milestone": {
|
||||
"title": "hi"
|
||||
}
|
||||
},
|
||||
{
|
||||
"milestone": null
|
||||
}
|
||||
]
|
||||
`), buf.String())
|
||||
}
|
||||
|
||||
func TestPullRequest_ExportData(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
@ -144,7 +122,10 @@ func TestPullRequest_ExportData(t *testing.T) {
|
|||
outputJSON: heredoc.Doc(`
|
||||
{
|
||||
"milestone": {
|
||||
"title": "The next big thing"
|
||||
"number": 0,
|
||||
"title": "The next big thing",
|
||||
"description": "",
|
||||
"dueOn": null
|
||||
},
|
||||
"number": 2345
|
||||
}
|
||||
|
|
@ -154,7 +135,7 @@ func TestPullRequest_ExportData(t *testing.T) {
|
|||
name: "status checks",
|
||||
fields: []string{"statusCheckRollup"},
|
||||
inputJSON: heredoc.Doc(`
|
||||
{ "commits": { "nodes": [
|
||||
{ "statusCheckRollup": { "nodes": [
|
||||
{ "commit": { "statusCheckRollup": { "contexts": { "nodes": [
|
||||
{
|
||||
"__typename": "CheckRun",
|
||||
|
|
|
|||
53
api/export_repo.go
Normal file
53
api/export_repo.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func (repo *Repository) ExportData(fields []string) *map[string]interface{} {
|
||||
v := reflect.ValueOf(repo).Elem()
|
||||
data := map[string]interface{}{}
|
||||
|
||||
for _, f := range fields {
|
||||
switch f {
|
||||
case "parent":
|
||||
data[f] = miniRepoExport(repo.Parent)
|
||||
case "templateRepository":
|
||||
data[f] = miniRepoExport(repo.TemplateRepository)
|
||||
case "languages":
|
||||
data[f] = repo.Languages.Edges
|
||||
case "labels":
|
||||
data[f] = repo.Labels.Nodes
|
||||
case "assignableUsers":
|
||||
data[f] = repo.AssignableUsers.Nodes
|
||||
case "mentionableUsers":
|
||||
data[f] = repo.MentionableUsers.Nodes
|
||||
case "milestones":
|
||||
data[f] = repo.Milestones.Nodes
|
||||
case "projects":
|
||||
data[f] = repo.Projects.Nodes
|
||||
case "repositoryTopics":
|
||||
var topics []RepositoryTopic
|
||||
for _, n := range repo.RepositoryTopics.Nodes {
|
||||
topics = append(topics, n.Topic)
|
||||
}
|
||||
data[f] = topics
|
||||
default:
|
||||
sf := fieldByName(v, f)
|
||||
data[f] = sf.Interface()
|
||||
}
|
||||
}
|
||||
|
||||
return &data
|
||||
}
|
||||
|
||||
func miniRepoExport(r *Repository) map[string]interface{} {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"id": r.ID,
|
||||
"name": r.Name,
|
||||
"owner": r.Owner,
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ import (
|
|||
func TestPullRequest_ChecksStatus(t *testing.T) {
|
||||
pr := PullRequest{}
|
||||
payload := `
|
||||
{ "commits": { "nodes": [{ "commit": {
|
||||
{ "statusCheckRollup": { "nodes": [{ "commit": {
|
||||
"statusCheckRollup": {
|
||||
"contexts": {
|
||||
"nodes": [
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/shurcooL/graphql"
|
||||
)
|
||||
|
|
@ -12,7 +11,10 @@ import (
|
|||
type Comments struct {
|
||||
Nodes []Comment
|
||||
TotalCount int
|
||||
PageInfo PageInfo
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
}
|
||||
}
|
||||
|
||||
type Comment struct {
|
||||
|
|
@ -26,83 +28,6 @@ type Comment struct {
|
|||
ReactionGroups ReactionGroups `json:"reactionGroups"`
|
||||
}
|
||||
|
||||
type PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
}
|
||||
|
||||
func CommentsForIssue(client *Client, repo ghrepo.Interface, issue *Issue) (*Comments, error) {
|
||||
type response struct {
|
||||
Repository struct {
|
||||
Issue struct {
|
||||
Comments Comments `graphql:"comments(first: 100, after: $endCursor)"`
|
||||
} `graphql:"issue(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(issue.Number),
|
||||
"endCursor": (*githubv4.String)(nil),
|
||||
}
|
||||
|
||||
gql := graphQLClient(client.http, repo.RepoHost())
|
||||
|
||||
var comments []Comment
|
||||
for {
|
||||
var query response
|
||||
err := gql.QueryNamed(context.Background(), "CommentsForIssue", &query, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
comments = append(comments, query.Repository.Issue.Comments.Nodes...)
|
||||
if !query.Repository.Issue.Comments.PageInfo.HasNextPage {
|
||||
break
|
||||
}
|
||||
variables["endCursor"] = githubv4.String(query.Repository.Issue.Comments.PageInfo.EndCursor)
|
||||
}
|
||||
|
||||
return &Comments{Nodes: comments, TotalCount: len(comments)}, nil
|
||||
}
|
||||
|
||||
func CommentsForPullRequest(client *Client, repo ghrepo.Interface, pr *PullRequest) (*Comments, error) {
|
||||
type response struct {
|
||||
Repository struct {
|
||||
PullRequest struct {
|
||||
Comments Comments `graphql:"comments(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 comments []Comment
|
||||
for {
|
||||
var query response
|
||||
err := gql.QueryNamed(context.Background(), "CommentsForPullRequest", &query, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
comments = append(comments, query.Repository.PullRequest.Comments.Nodes...)
|
||||
if !query.Repository.PullRequest.Comments.PageInfo.HasNextPage {
|
||||
break
|
||||
}
|
||||
variables["endCursor"] = githubv4.String(query.Repository.PullRequest.Comments.PageInfo.EndCursor)
|
||||
}
|
||||
|
||||
return &Comments{Nodes: comments, TotalCount: len(comments)}, nil
|
||||
}
|
||||
|
||||
type CommentCreateInput struct {
|
||||
Body string
|
||||
SubjectId string
|
||||
|
|
@ -135,24 +60,6 @@ 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
|
||||
isMinimized
|
||||
minimizedReason
|
||||
` + reactionGroupsFragment() + `
|
||||
}
|
||||
totalCount
|
||||
}`
|
||||
}
|
||||
|
||||
func (c Comment) AuthorLogin() string {
|
||||
return c.Author.Login
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,14 +36,12 @@ type Issue struct {
|
|||
Assignees Assignees
|
||||
Labels Labels
|
||||
ProjectCards ProjectCards
|
||||
Milestone Milestone
|
||||
Milestone *Milestone
|
||||
ReactionGroups ReactionGroups
|
||||
}
|
||||
|
||||
type Assignees struct {
|
||||
Nodes []struct {
|
||||
Login string `json:"login"`
|
||||
}
|
||||
Nodes []GitHubUser
|
||||
TotalCount int
|
||||
}
|
||||
|
||||
|
|
@ -56,9 +54,7 @@ func (a Assignees) Logins() []string {
|
|||
}
|
||||
|
||||
type Labels struct {
|
||||
Nodes []struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
Nodes []IssueLabel
|
||||
TotalCount int
|
||||
}
|
||||
|
||||
|
|
@ -91,14 +87,26 @@ func (p ProjectCards) ProjectNames() []string {
|
|||
}
|
||||
|
||||
type Milestone struct {
|
||||
Title string `json:"title"`
|
||||
Number int `json:"number"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
DueOn *time.Time `json:"dueOn"`
|
||||
}
|
||||
|
||||
type IssuesDisabledError struct {
|
||||
error
|
||||
}
|
||||
|
||||
type Owner struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Login string `json:"login"`
|
||||
}
|
||||
|
||||
type Author struct {
|
||||
// adding these breaks generated GraphQL requests
|
||||
//ID string `json:"id,omitempty"`
|
||||
//Name string `json:"name,omitempty"`
|
||||
Login string `json:"login"`
|
||||
}
|
||||
|
||||
|
|
@ -266,13 +274,18 @@ func IssueByNumber(client *Client, repo ghrepo.Interface, number int) (*Issue, e
|
|||
createdAt
|
||||
assignees(first: 100) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
login
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
labels(first: 100) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
description
|
||||
color
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
|
|
@ -288,7 +301,10 @@ func IssueByNumber(client *Client, repo ghrepo.Interface, number int) (*Issue, e
|
|||
totalCount
|
||||
}
|
||||
milestone {
|
||||
number
|
||||
title
|
||||
description
|
||||
dueOn
|
||||
}
|
||||
reactionGroups {
|
||||
content
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -34,6 +33,7 @@ type PullRequest struct {
|
|||
Number int
|
||||
Title string
|
||||
State string
|
||||
Closed bool
|
||||
URL string
|
||||
BaseRefName string
|
||||
HeadRefName string
|
||||
|
|
@ -57,12 +57,8 @@ type PullRequest struct {
|
|||
|
||||
Author Author
|
||||
MergedBy *Author
|
||||
HeadRepositoryOwner struct {
|
||||
Login string `json:"login"`
|
||||
}
|
||||
HeadRepository struct {
|
||||
Name string
|
||||
}
|
||||
HeadRepositoryOwner Owner
|
||||
HeadRepository *PRRepository
|
||||
IsCrossRepository bool
|
||||
IsDraft bool
|
||||
MaintainerCanModify bool
|
||||
|
|
@ -77,9 +73,11 @@ type PullRequest struct {
|
|||
|
||||
Commits struct {
|
||||
TotalCount int
|
||||
Nodes []struct {
|
||||
Nodes []PullRequestCommit
|
||||
}
|
||||
StatusCheckRollup struct {
|
||||
Nodes []struct {
|
||||
Commit struct {
|
||||
Oid string
|
||||
StatusCheckRollup struct {
|
||||
Contexts struct {
|
||||
Nodes []struct {
|
||||
|
|
@ -94,25 +92,56 @@ type PullRequest struct {
|
|||
DetailsURL string `json:"detailsUrl"`
|
||||
TargetURL string `json:"targetUrl,omitempty"`
|
||||
}
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Assignees Assignees
|
||||
Labels Labels
|
||||
ProjectCards ProjectCards
|
||||
Milestone Milestone
|
||||
Milestone *Milestone
|
||||
Comments Comments
|
||||
ReactionGroups ReactionGroups
|
||||
Reviews PullRequestReviews
|
||||
ReviewRequests ReviewRequests
|
||||
}
|
||||
|
||||
type PRRepository struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Commit loads just the commit SHA and nothing else
|
||||
type Commit struct {
|
||||
OID string `json:"oid"`
|
||||
}
|
||||
|
||||
type PullRequestCommit struct {
|
||||
Commit PullRequestCommitCommit
|
||||
}
|
||||
|
||||
// PullRequestCommitCommit contains full information about a commit
|
||||
type PullRequestCommitCommit struct {
|
||||
OID string `json:"oid"`
|
||||
Authors struct {
|
||||
Nodes []struct {
|
||||
Name string
|
||||
Email string
|
||||
User GitHubUser
|
||||
}
|
||||
}
|
||||
MessageHeadline string
|
||||
MessageBody string
|
||||
CommittedDate time.Time
|
||||
AuthoredDate time.Time
|
||||
}
|
||||
|
||||
type PullRequestFile struct {
|
||||
Path string `json:"path"`
|
||||
Additions int `json:"additions"`
|
||||
|
|
@ -127,7 +156,6 @@ type ReviewRequests struct {
|
|||
Name string `json:"name"`
|
||||
}
|
||||
}
|
||||
TotalCount int
|
||||
}
|
||||
|
||||
func (r ReviewRequests) Logins() []string {
|
||||
|
|
@ -138,14 +166,6 @@ func (r ReviewRequests) Logins() []string {
|
|||
return logins
|
||||
}
|
||||
|
||||
type NotFoundError struct {
|
||||
error
|
||||
}
|
||||
|
||||
func (err *NotFoundError) Unwrap() error {
|
||||
return err.error
|
||||
}
|
||||
|
||||
func (pr PullRequest) HeadLabel() string {
|
||||
if pr.IsCrossRepository {
|
||||
return fmt.Sprintf("%s:%s", pr.HeadRepositoryOwner.Login, pr.HeadRefName)
|
||||
|
|
@ -192,10 +212,10 @@ type PullRequestChecksStatus struct {
|
|||
}
|
||||
|
||||
func (pr *PullRequest) ChecksStatus() (summary PullRequestChecksStatus) {
|
||||
if len(pr.Commits.Nodes) == 0 {
|
||||
if len(pr.StatusCheckRollup.Nodes) == 0 {
|
||||
return
|
||||
}
|
||||
commit := pr.Commits.Nodes[0].Commit
|
||||
commit := pr.StatusCheckRollup.Nodes[0].Commit
|
||||
for _, c := range commit.StatusCheckRollup.Contexts.Nodes {
|
||||
state := c.State // StatusContext
|
||||
if state == "" {
|
||||
|
|
@ -247,7 +267,7 @@ func (c Client) PullRequestDiff(baseRepo ghrepo.Interface, prNumber int) (io.Rea
|
|||
}
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return nil, &NotFoundError{errors.New("pull request not found")}
|
||||
return nil, errors.New("pull request not found")
|
||||
} else if resp.StatusCode != 200 {
|
||||
return nil, HandleHTTPError(resp)
|
||||
}
|
||||
|
|
@ -560,274 +580,6 @@ func pullRequestFragment(httpClient *http.Client, hostname string) (string, erro
|
|||
return fragments, nil
|
||||
}
|
||||
|
||||
func prCommitsFragment(httpClient *http.Client, hostname string) (string, error) {
|
||||
cachedClient := NewCachedClient(httpClient, time.Hour*24)
|
||||
if prFeatures, err := determinePullRequestFeatures(cachedClient, hostname); err != nil {
|
||||
return "", err
|
||||
} else if !prFeatures.HasStatusCheckRollup {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return `
|
||||
commits(last: 1) {
|
||||
totalCount
|
||||
nodes {
|
||||
commit {
|
||||
oid
|
||||
statusCheckRollup {
|
||||
contexts(last: 100) {
|
||||
nodes {
|
||||
...on StatusContext {
|
||||
context
|
||||
state
|
||||
targetUrl
|
||||
}
|
||||
...on CheckRun {
|
||||
name
|
||||
status
|
||||
conclusion
|
||||
startedAt
|
||||
completedAt
|
||||
detailsUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`, nil
|
||||
}
|
||||
|
||||
func PullRequestByNumber(client *Client, repo ghrepo.Interface, number int) (*PullRequest, error) {
|
||||
type response struct {
|
||||
Repository struct {
|
||||
PullRequest PullRequest
|
||||
}
|
||||
}
|
||||
|
||||
statusesFragment, err := prCommitsFragment(client.http, repo.RepoHost())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := `
|
||||
query PullRequestByNumber($owner: String!, $repo: String!, $pr_number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $pr_number) {
|
||||
id
|
||||
url
|
||||
number
|
||||
title
|
||||
state
|
||||
closed
|
||||
body
|
||||
mergeable
|
||||
additions
|
||||
deletions
|
||||
author {
|
||||
login
|
||||
}
|
||||
` + statusesFragment + `
|
||||
baseRefName
|
||||
headRefName
|
||||
headRepositoryOwner {
|
||||
login
|
||||
}
|
||||
headRepository {
|
||||
name
|
||||
}
|
||||
isCrossRepository
|
||||
isDraft
|
||||
maintainerCanModify
|
||||
reviewRequests(first: 100) {
|
||||
nodes {
|
||||
requestedReviewer {
|
||||
__typename
|
||||
...on User {
|
||||
login
|
||||
}
|
||||
...on Team {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
assignees(first: 100) {
|
||||
nodes {
|
||||
login
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
labels(first: 100) {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
projectCards(first: 100) {
|
||||
nodes {
|
||||
project {
|
||||
name
|
||||
}
|
||||
column {
|
||||
name
|
||||
}
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
milestone{
|
||||
title
|
||||
}
|
||||
` + commentsFragment() + `
|
||||
` + reactionGroupsFragment() + `
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"owner": repo.RepoOwner(),
|
||||
"repo": repo.RepoName(),
|
||||
"pr_number": number,
|
||||
}
|
||||
|
||||
var resp response
|
||||
err = client.GraphQL(repo.RepoHost(), query, variables, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &resp.Repository.PullRequest, nil
|
||||
}
|
||||
|
||||
func PullRequestForBranch(client *Client, repo ghrepo.Interface, baseBranch, headBranch string, stateFilters []string) (*PullRequest, error) {
|
||||
type response struct {
|
||||
Repository struct {
|
||||
PullRequests struct {
|
||||
Nodes []PullRequest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
statusesFragment, err := prCommitsFragment(client.http, repo.RepoHost())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := `
|
||||
query PullRequestForBranch($owner: String!, $repo: String!, $headRefName: String!, $states: [PullRequestState!]) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequests(headRefName: $headRefName, states: $states, first: 30, orderBy: { field: CREATED_AT, direction: DESC }) {
|
||||
nodes {
|
||||
id
|
||||
number
|
||||
title
|
||||
state
|
||||
body
|
||||
mergeable
|
||||
additions
|
||||
deletions
|
||||
author {
|
||||
login
|
||||
}
|
||||
` + statusesFragment + `
|
||||
url
|
||||
baseRefName
|
||||
headRefName
|
||||
headRepositoryOwner {
|
||||
login
|
||||
}
|
||||
headRepository {
|
||||
name
|
||||
}
|
||||
isCrossRepository
|
||||
isDraft
|
||||
maintainerCanModify
|
||||
reviewRequests(first: 100) {
|
||||
nodes {
|
||||
requestedReviewer {
|
||||
__typename
|
||||
...on User {
|
||||
login
|
||||
}
|
||||
...on Team {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
assignees(first: 100) {
|
||||
nodes {
|
||||
login
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
labels(first: 100) {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
projectCards(first: 100) {
|
||||
nodes {
|
||||
project {
|
||||
name
|
||||
}
|
||||
column {
|
||||
name
|
||||
}
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
milestone{
|
||||
title
|
||||
}
|
||||
` + commentsFragment() + `
|
||||
` + reactionGroupsFragment() + `
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
branchWithoutOwner := headBranch
|
||||
if idx := strings.Index(headBranch, ":"); idx >= 0 {
|
||||
branchWithoutOwner = headBranch[idx+1:]
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"owner": repo.RepoOwner(),
|
||||
"repo": repo.RepoName(),
|
||||
"headRefName": branchWithoutOwner,
|
||||
"states": stateFilters,
|
||||
}
|
||||
|
||||
var resp response
|
||||
err = client.GraphQL(repo.RepoHost(), query, variables, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prs := resp.Repository.PullRequests.Nodes
|
||||
sortPullRequestsByState(prs)
|
||||
|
||||
for _, pr := range prs {
|
||||
if pr.HeadLabel() == headBranch && (baseBranch == "" || pr.BaseRefName == baseBranch) {
|
||||
return &pr, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, &NotFoundError{fmt.Errorf("no pull requests found for branch %q", headBranch)}
|
||||
}
|
||||
|
||||
// sortPullRequestsByState sorts a PullRequest slice by open-first
|
||||
func sortPullRequestsByState(prs []PullRequest) {
|
||||
sort.SliceStable(prs, func(a, b int) bool {
|
||||
return prs[a].State == "OPEN"
|
||||
})
|
||||
}
|
||||
|
||||
// CreatePullRequest creates a pull request in a GitHub repository
|
||||
func CreatePullRequest(client *Client, repo *Repository, params map[string]interface{}) (*PullRequest, error) {
|
||||
query := `
|
||||
|
|
|
|||
|
|
@ -22,8 +22,11 @@ type PullRequestReviewInput struct {
|
|||
}
|
||||
|
||||
type PullRequestReviews struct {
|
||||
Nodes []PullRequestReview
|
||||
PageInfo PageInfo
|
||||
Nodes []PullRequestReview
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
}
|
||||
TotalCount int
|
||||
}
|
||||
|
||||
|
|
@ -66,42 +69,6 @@ func AddReview(client *Client, repo ghrepo.Interface, pr *PullRequest, input *Pu
|
|||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,32 +158,3 @@ func Test_determinePullRequestFeatures(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_sortPullRequestsByState(t *testing.T) {
|
||||
prs := []PullRequest{
|
||||
{
|
||||
BaseRefName: "test1",
|
||||
State: "MERGED",
|
||||
},
|
||||
{
|
||||
BaseRefName: "test2",
|
||||
State: "CLOSED",
|
||||
},
|
||||
{
|
||||
BaseRefName: "test3",
|
||||
State: "OPEN",
|
||||
},
|
||||
}
|
||||
|
||||
sortPullRequestsByState(prs)
|
||||
|
||||
if prs[0].BaseRefName != "test3" {
|
||||
t.Errorf("prs[0]: got %s, want %q", prs[0].BaseRefName, "test3")
|
||||
}
|
||||
if prs[1].BaseRefName != "test1" {
|
||||
t.Errorf("prs[1]: got %s, want %q", prs[1].BaseRefName, "test1")
|
||||
}
|
||||
if prs[2].BaseRefName != "test2" {
|
||||
t.Errorf("prs[2]: got %s, want %q", prs[2].BaseRefName, "test2")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,25 +16,100 @@ import (
|
|||
|
||||
// Repository contains information about a GitHub repo
|
||||
type Repository struct {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
URL string
|
||||
CloneURL string
|
||||
CreatedAt time.Time
|
||||
Owner RepositoryOwner
|
||||
ID string
|
||||
Name string
|
||||
NameWithOwner string
|
||||
Owner RepositoryOwner
|
||||
Parent *Repository
|
||||
TemplateRepository *Repository
|
||||
Description string
|
||||
HomepageURL string
|
||||
OpenGraphImageURL string
|
||||
UsesCustomOpenGraphImage bool
|
||||
URL string
|
||||
SSHURL string
|
||||
MirrorURL string
|
||||
SecurityPolicyURL string
|
||||
|
||||
IsPrivate bool
|
||||
HasIssuesEnabled bool
|
||||
HasWikiEnabled bool
|
||||
ViewerPermission string
|
||||
DefaultBranchRef BranchRef
|
||||
CreatedAt time.Time
|
||||
PushedAt *time.Time
|
||||
UpdatedAt time.Time
|
||||
|
||||
Parent *Repository
|
||||
IsBlankIssuesEnabled bool
|
||||
IsSecurityPolicyEnabled bool
|
||||
HasIssuesEnabled bool
|
||||
HasProjectsEnabled bool
|
||||
HasWikiEnabled bool
|
||||
MergeCommitAllowed bool
|
||||
SquashMergeAllowed bool
|
||||
RebaseMergeAllowed bool
|
||||
|
||||
MergeCommitAllowed bool
|
||||
RebaseMergeAllowed bool
|
||||
SquashMergeAllowed bool
|
||||
ForkCount int
|
||||
StargazerCount int
|
||||
Watchers struct {
|
||||
TotalCount int `json:"totalCount"`
|
||||
}
|
||||
Issues struct {
|
||||
TotalCount int `json:"totalCount"`
|
||||
}
|
||||
PullRequests struct {
|
||||
TotalCount int `json:"totalCount"`
|
||||
}
|
||||
|
||||
CodeOfConduct *CodeOfConduct
|
||||
ContactLinks []ContactLink
|
||||
DefaultBranchRef BranchRef
|
||||
DeleteBranchOnMerge bool
|
||||
DiskUsage int
|
||||
FundingLinks []FundingLink
|
||||
IsArchived bool
|
||||
IsEmpty bool
|
||||
IsFork bool
|
||||
IsInOrganization bool
|
||||
IsMirror bool
|
||||
IsPrivate bool
|
||||
IsTemplate bool
|
||||
IsUserConfigurationRepository bool
|
||||
LicenseInfo *RepositoryLicense
|
||||
ViewerCanAdminister bool
|
||||
ViewerDefaultCommitEmail string
|
||||
ViewerDefaultMergeMethod string
|
||||
ViewerHasStarred bool
|
||||
ViewerPermission string
|
||||
ViewerPossibleCommitEmails []string
|
||||
ViewerSubscription string
|
||||
|
||||
RepositoryTopics struct {
|
||||
Nodes []struct {
|
||||
Topic RepositoryTopic
|
||||
}
|
||||
}
|
||||
PrimaryLanguage *CodingLanguage
|
||||
Languages struct {
|
||||
Edges []struct {
|
||||
Size int `json:"size"`
|
||||
Node CodingLanguage `json:"node"`
|
||||
}
|
||||
}
|
||||
IssueTemplates []IssueTemplate
|
||||
PullRequestTemplates []PullRequestTemplate
|
||||
Labels struct {
|
||||
Nodes []IssueLabel
|
||||
}
|
||||
Milestones struct {
|
||||
Nodes []Milestone
|
||||
}
|
||||
LatestRelease *RepositoryRelease
|
||||
|
||||
AssignableUsers struct {
|
||||
Nodes []GitHubUser
|
||||
}
|
||||
MentionableUsers struct {
|
||||
Nodes []GitHubUser
|
||||
}
|
||||
Projects struct {
|
||||
Nodes []RepoProject
|
||||
}
|
||||
|
||||
// pseudo-field that keeps track of host name of this repo
|
||||
hostname string
|
||||
|
|
@ -42,12 +117,76 @@ type Repository struct {
|
|||
|
||||
// RepositoryOwner is the owner of a GitHub repository
|
||||
type RepositoryOwner struct {
|
||||
Login string
|
||||
ID string `json:"id"`
|
||||
Login string `json:"login"`
|
||||
}
|
||||
|
||||
type GitHubUser struct {
|
||||
ID string `json:"id"`
|
||||
Login string `json:"login"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// BranchRef is the branch name in a GitHub repository
|
||||
type BranchRef struct {
|
||||
Name string
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type CodeOfConduct struct {
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type RepositoryLicense struct {
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
Nickname string `json:"nickname"`
|
||||
}
|
||||
|
||||
type ContactLink struct {
|
||||
About string `json:"about"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type FundingLink struct {
|
||||
Platform string `json:"platform"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type CodingLanguage struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type IssueTemplate struct {
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
About string `json:"about"`
|
||||
}
|
||||
|
||||
type PullRequestTemplate struct {
|
||||
Filename string `json:"filename"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
type RepositoryTopic struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type RepositoryRelease struct {
|
||||
Name string `json:"name"`
|
||||
TagName string `json:"tagName"`
|
||||
URL string `json:"url"`
|
||||
PublishedAt time.Time `json:"publishedAt"`
|
||||
}
|
||||
|
||||
type IssueLabel struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Color string `json:"color"`
|
||||
}
|
||||
|
||||
// RepoOwner is the login name of the owner
|
||||
|
|
@ -65,11 +204,6 @@ func (r Repository) RepoHost() string {
|
|||
return r.hostname
|
||||
}
|
||||
|
||||
// IsFork is true when this repository has a parent repository
|
||||
func (r Repository) IsFork() bool {
|
||||
return r.Parent != nil
|
||||
}
|
||||
|
||||
// ViewerCanPush is true when the requesting user has push access
|
||||
func (r Repository) ViewerCanPush() bool {
|
||||
switch r.ViewerPermission {
|
||||
|
|
@ -305,16 +439,26 @@ type repositoryV3 struct {
|
|||
NodeID string
|
||||
Name string
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
CloneURL string `json:"clone_url"`
|
||||
Owner struct {
|
||||
Login string
|
||||
}
|
||||
}
|
||||
|
||||
// ForkRepo forks the repository on GitHub and returns the new repository
|
||||
func ForkRepo(client *Client, repo ghrepo.Interface) (*Repository, error) {
|
||||
func ForkRepo(client *Client, repo ghrepo.Interface, org string) (*Repository, error) {
|
||||
path := fmt.Sprintf("repos/%s/forks", ghrepo.FullName(repo))
|
||||
body := bytes.NewBufferString(`{}`)
|
||||
|
||||
params := map[string]interface{}{}
|
||||
if org != "" {
|
||||
params["organization"] = org
|
||||
}
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
enc := json.NewEncoder(body)
|
||||
if err := enc.Encode(params); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := repositoryV3{}
|
||||
err := client.REST(repo.RepoHost(), "POST", path, body, &result)
|
||||
if err != nil {
|
||||
|
|
@ -324,7 +468,6 @@ func ForkRepo(client *Client, repo ghrepo.Interface) (*Repository, error) {
|
|||
return &Repository{
|
||||
ID: result.NodeID,
|
||||
Name: result.Name,
|
||||
CloneURL: result.CloneURL,
|
||||
CreatedAt: result.CreatedAt,
|
||||
Owner: RepositoryOwner{
|
||||
Login: result.Owner.Login,
|
||||
|
|
@ -707,9 +850,10 @@ func RepoResolveMetadataIDs(client *Client, repo ghrepo.Interface, input RepoRes
|
|||
}
|
||||
|
||||
type RepoProject struct {
|
||||
ID string
|
||||
Name string
|
||||
ResourcePath string
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Number int `json:"number"`
|
||||
ResourcePath string `json:"resourcePath"`
|
||||
}
|
||||
|
||||
// RepoProjects fetches all open projects for a repository
|
||||
|
|
|
|||
|
|
@ -144,9 +144,9 @@ 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"},
|
||||
{ID: "id1", Name: "My Project", ResourcePath: "/OWNER/REPO/projects/PROJECT_NUMBER"},
|
||||
{ID: "id2", Name: "Org Project", ResourcePath: "/orgs/ORG/projects/PROJECT_NUMBER"},
|
||||
{ID: "id3", Name: "Project", ResourcePath: "/orgs/ORG/projects/PROJECT_NUMBER_2"},
|
||||
}
|
||||
projectNames := []string{"My Project", "Org Project"}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
|
@ -18,7 +19,7 @@ func shortenQuery(q string) string {
|
|||
}
|
||||
|
||||
var issueComments = shortenQuery(`
|
||||
comments(last: 100) {
|
||||
comments(first: 100) {
|
||||
nodes {
|
||||
author{login},
|
||||
authorAssociation,
|
||||
|
|
@ -29,25 +30,25 @@ var issueComments = shortenQuery(`
|
|||
minimizedReason,
|
||||
reactionGroups{content,users{totalCount}}
|
||||
},
|
||||
pageInfo{hasNextPage,endCursor},
|
||||
totalCount
|
||||
}
|
||||
`)
|
||||
|
||||
var prReviewRequests = shortenQuery(`
|
||||
reviewRequests(last: 100) {
|
||||
reviewRequests(first: 100) {
|
||||
nodes {
|
||||
requestedReviewer {
|
||||
__typename,
|
||||
...on User{login},
|
||||
...on Team{name}
|
||||
}
|
||||
},
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
var prReviews = shortenQuery(`
|
||||
reviews(last: 100) {
|
||||
reviews(first: 100) {
|
||||
nodes {
|
||||
author{login},
|
||||
authorAssociation,
|
||||
|
|
@ -56,6 +57,7 @@ var prReviews = shortenQuery(`
|
|||
state,
|
||||
reactionGroups{content,users{totalCount}}
|
||||
}
|
||||
pageInfo{hasNextPage,endCursor}
|
||||
}
|
||||
`)
|
||||
|
||||
|
|
@ -69,14 +71,38 @@ var prFiles = shortenQuery(`
|
|||
}
|
||||
`)
|
||||
|
||||
var prStatusCheckRollup = shortenQuery(`
|
||||
commits(last: 1) {
|
||||
totalCount,
|
||||
var prCommits = shortenQuery(`
|
||||
commits(first: 100) {
|
||||
nodes {
|
||||
commit {
|
||||
authors(first:100) {
|
||||
nodes {
|
||||
name,
|
||||
email,
|
||||
user{id,login}
|
||||
}
|
||||
},
|
||||
messageHeadline,
|
||||
messageBody,
|
||||
oid,
|
||||
committedDate,
|
||||
authoredDate
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
func StatusCheckRollupGraphQL(after string) string {
|
||||
var afterClause string
|
||||
if after != "" {
|
||||
afterClause = ",after:" + after
|
||||
}
|
||||
return fmt.Sprintf(shortenQuery(`
|
||||
statusCheckRollup: commits(last: 1) {
|
||||
nodes {
|
||||
commit {
|
||||
statusCheckRollup {
|
||||
contexts(last: 100) {
|
||||
contexts(first:100%s) {
|
||||
nodes {
|
||||
__typename
|
||||
...on StatusContext {
|
||||
|
|
@ -92,13 +118,14 @@ var prStatusCheckRollup = shortenQuery(`
|
|||
completedAt,
|
||||
detailsUrl
|
||||
}
|
||||
}
|
||||
},
|
||||
pageInfo{hasNextPage,endCursor}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
}`), afterClause)
|
||||
}
|
||||
|
||||
var IssueFields = []string{
|
||||
"assignees",
|
||||
|
|
@ -124,6 +151,7 @@ var PullRequestFields = append(IssueFields,
|
|||
"additions",
|
||||
"baseRefName",
|
||||
"changedFiles",
|
||||
"commits",
|
||||
"deletions",
|
||||
"files",
|
||||
"headRefName",
|
||||
|
|
@ -153,17 +181,17 @@ func PullRequestGraphQL(fields []string) string {
|
|||
case "mergedBy":
|
||||
q = append(q, `mergedBy{login}`)
|
||||
case "headRepositoryOwner":
|
||||
q = append(q, `headRepositoryOwner{login}`)
|
||||
q = append(q, `headRepositoryOwner{id,login,...on User{name}}`)
|
||||
case "headRepository":
|
||||
q = append(q, `headRepository{name}`)
|
||||
q = append(q, `headRepository{id,name}`)
|
||||
case "assignees":
|
||||
q = append(q, `assignees(first:100){nodes{login},totalCount}`)
|
||||
q = append(q, `assignees(first:100){nodes{id,login,name},totalCount}`)
|
||||
case "labels":
|
||||
q = append(q, `labels(first:100){nodes{name},totalCount}`)
|
||||
q = append(q, `labels(first:100){nodes{id,name,description,color},totalCount}`)
|
||||
case "projectCards":
|
||||
q = append(q, `projectCards(first:100){nodes{project{name}column{name}},totalCount}`)
|
||||
case "milestone":
|
||||
q = append(q, `milestone{title}`)
|
||||
q = append(q, `milestone{number,title,description,dueOn}`)
|
||||
case "reactionGroups":
|
||||
q = append(q, `reactionGroups{content,users{totalCount}}`)
|
||||
case "mergeCommit":
|
||||
|
|
@ -178,8 +206,142 @@ func PullRequestGraphQL(fields []string) string {
|
|||
q = append(q, prReviews)
|
||||
case "files":
|
||||
q = append(q, prFiles)
|
||||
case "commits":
|
||||
q = append(q, prCommits)
|
||||
case "commitsCount": // pseudo-field
|
||||
q = append(q, `commits{totalCount}`)
|
||||
case "statusCheckRollup":
|
||||
q = append(q, prStatusCheckRollup)
|
||||
q = append(q, StatusCheckRollupGraphQL(""))
|
||||
default:
|
||||
q = append(q, field)
|
||||
}
|
||||
}
|
||||
return strings.Join(q, ",")
|
||||
}
|
||||
|
||||
var RepositoryFields = []string{
|
||||
"id",
|
||||
"name",
|
||||
"nameWithOwner",
|
||||
"owner",
|
||||
"parent",
|
||||
"templateRepository",
|
||||
"description",
|
||||
"homepageUrl",
|
||||
"openGraphImageUrl",
|
||||
"usesCustomOpenGraphImage",
|
||||
"url",
|
||||
"sshUrl",
|
||||
"mirrorUrl",
|
||||
"securityPolicyUrl",
|
||||
|
||||
"createdAt",
|
||||
"pushedAt",
|
||||
"updatedAt",
|
||||
|
||||
"isBlankIssuesEnabled",
|
||||
"isSecurityPolicyEnabled",
|
||||
"hasIssuesEnabled",
|
||||
"hasProjectsEnabled",
|
||||
"hasWikiEnabled",
|
||||
"mergeCommitAllowed",
|
||||
"squashMergeAllowed",
|
||||
"rebaseMergeAllowed",
|
||||
|
||||
"forkCount",
|
||||
"stargazerCount",
|
||||
"watchers",
|
||||
"issues",
|
||||
"pullRequests",
|
||||
|
||||
"codeOfConduct",
|
||||
"contactLinks",
|
||||
"defaultBranchRef",
|
||||
"deleteBranchOnMerge",
|
||||
"diskUsage",
|
||||
"fundingLinks",
|
||||
"isArchived",
|
||||
"isEmpty",
|
||||
"isFork",
|
||||
"isInOrganization",
|
||||
"isMirror",
|
||||
"isPrivate",
|
||||
"isTemplate",
|
||||
"isUserConfigurationRepository",
|
||||
"licenseInfo",
|
||||
"viewerCanAdminister",
|
||||
"viewerDefaultCommitEmail",
|
||||
"viewerDefaultMergeMethod",
|
||||
"viewerHasStarred",
|
||||
"viewerPermission",
|
||||
"viewerPossibleCommitEmails",
|
||||
"viewerSubscription",
|
||||
|
||||
"repositoryTopics",
|
||||
"primaryLanguage",
|
||||
"languages",
|
||||
"issueTemplates",
|
||||
"pullRequestTemplates",
|
||||
"labels",
|
||||
"milestones",
|
||||
"latestRelease",
|
||||
|
||||
"assignableUsers",
|
||||
"mentionableUsers",
|
||||
"projects",
|
||||
|
||||
// "branchProtectionRules", // too complex to expose
|
||||
// "collaborators", // does it make sense to expose without affiliation filter?
|
||||
}
|
||||
|
||||
func RepositoryGraphQL(fields []string) string {
|
||||
var q []string
|
||||
for _, field := range fields {
|
||||
switch field {
|
||||
case "codeOfConduct":
|
||||
q = append(q, "codeOfConduct{key,name,url}")
|
||||
case "contactLinks":
|
||||
q = append(q, "contactLinks{about,name,url}")
|
||||
case "fundingLinks":
|
||||
q = append(q, "fundingLinks{platform,url}")
|
||||
case "licenseInfo":
|
||||
q = append(q, "licenseInfo{key,name,nickname}")
|
||||
case "owner":
|
||||
q = append(q, "owner{id,login}")
|
||||
case "parent":
|
||||
q = append(q, "parent{id,name,owner{id,login}}")
|
||||
case "templateRepository":
|
||||
q = append(q, "templateRepository{id,name,owner{id,login}}")
|
||||
case "repositoryTopics":
|
||||
q = append(q, "repositoryTopics(first:100){nodes{topic{name}}}")
|
||||
case "issueTemplates":
|
||||
q = append(q, "issueTemplates{name,title,body,about}")
|
||||
case "pullRequestTemplates":
|
||||
q = append(q, "pullRequestTemplates{body,filename}")
|
||||
case "labels":
|
||||
q = append(q, "labels(first:100){nodes{id,color,name,description}}")
|
||||
case "languages":
|
||||
q = append(q, "languages(first:100){edges{size,node{name}}}")
|
||||
case "primaryLanguage":
|
||||
q = append(q, "primaryLanguage{name}")
|
||||
case "latestRelease":
|
||||
q = append(q, "latestRelease{publishedAt,tagName,name,url}")
|
||||
case "milestones":
|
||||
q = append(q, "milestones(first:100,states:OPEN){nodes{number,title,description,dueOn}}")
|
||||
case "assignableUsers":
|
||||
q = append(q, "assignableUsers(first:100){nodes{id,login,name}}")
|
||||
case "mentionableUsers":
|
||||
q = append(q, "mentionableUsers(first:100){nodes{id,login,name}}")
|
||||
case "projects":
|
||||
q = append(q, "projects(first:100,states:OPEN){nodes{id,name,number,body,resourcePath}}")
|
||||
case "watchers":
|
||||
q = append(q, "watchers{totalCount}")
|
||||
case "issues":
|
||||
q = append(q, "issues(states:OPEN){totalCount}")
|
||||
case "pullRequests":
|
||||
q = append(q, "pullRequests(states:OPEN){totalCount}")
|
||||
case "defaultBranchRef":
|
||||
q = append(q, "defaultBranchRef{name}")
|
||||
default:
|
||||
q = append(q, field)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ func TestPullRequestGraphQL(t *testing.T) {
|
|||
{
|
||||
name: "fields with nested structures",
|
||||
fields: []string{"author", "assignees"},
|
||||
want: "author{login},assignees(first:100){nodes{login},totalCount}",
|
||||
want: "author{login},assignees(first:100){nodes{id,login,name},totalCount}",
|
||||
},
|
||||
{
|
||||
name: "compressed query",
|
||||
|
|
|
|||
|
|
@ -57,12 +57,3 @@ var reactionEmoji = map[string]string{
|
|||
"ROCKET": "\U0001f680",
|
||||
"EYES": "\U0001f440",
|
||||
}
|
||||
|
||||
func reactionGroupsFragment() string {
|
||||
return `reactionGroups {
|
||||
content
|
||||
users {
|
||||
totalCount
|
||||
}
|
||||
}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ func (r *ResolvedRemotes) BaseRepo(io *iostreams.IOStreams) (ghrepo.Interface, e
|
|||
if repo == nil {
|
||||
continue
|
||||
}
|
||||
if repo.IsFork() {
|
||||
if repo.Parent != nil {
|
||||
add(repo.Parent)
|
||||
}
|
||||
add(repo)
|
||||
|
|
|
|||
84
docs/project-layout.md
Normal file
84
docs/project-layout.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# GitHub CLI project layout
|
||||
|
||||
At a high level, these areas make up the `github.com/cli/cli` project:
|
||||
- [`cmd/`](../cmd) - `main` packages for building binaries such as the `gh` executable
|
||||
- [`pkg/`](../pkg) - most other packages, including the implementation for individual gh commands
|
||||
- [`docs/`](../docs) - documentation for maintainers and contributors
|
||||
- [`script/`](../script) - build and release scripts
|
||||
- [`internal/`](../internal) - Go packages highly specific to our needs and thus internal
|
||||
- [`go.mod`](../go.mod) - external Go dependencies for this project, automatically fetched by Go at build time
|
||||
|
||||
Some auxiliary Go packages are at the top level of the project for historical reasons:
|
||||
- [`api/`](../api) - main utilities for making requests to the GitHub API
|
||||
- [`context/`](../context) - DEPRECATED: use only for referencing git remotes
|
||||
- [`git/`](../git) - utilities to gather information from a local git repository
|
||||
- [`test/`](../test) - DEPRECATED: do not use
|
||||
- [`utils/`](../utils) - DEPRECATED: use only for printing table output
|
||||
|
||||
## Command-line help text
|
||||
|
||||
Running `gh help issue list` displays help text for a topic. In this case, the topic is a specific command,
|
||||
and help text for every command is embedded in that command's source code. The naming convention for gh
|
||||
commands is:
|
||||
```
|
||||
pkg/cmd/<command>/<subcommand>/<subcommand>.go
|
||||
```
|
||||
Following the above example, the main implementation for the `gh issue list` command, including its help
|
||||
text, is in [pkg/cmd/issue/view/view.go](../pkg/cmd/issue/view/view.go)
|
||||
|
||||
Other help topics not specific to any command, for example `gh help environment`, are found in
|
||||
[pkg/cmd/root/help_topic.go](../pkg/cmd/root/help_topic.go).
|
||||
|
||||
During our release process, these help topics are [automatically converted](../cmd/gen-docs/main.go) to
|
||||
manual pages and published under https://cli.github.com/manual/.
|
||||
|
||||
## How GitHub CLI works
|
||||
|
||||
To illustrate how GitHub CLI works in its typical mode of operation, let's build the project, run a command,
|
||||
and talk through which code gets run in order.
|
||||
|
||||
1. `go run script/build.go` - Makes sure all external Go depedencies are fetched, then compiles the
|
||||
`cmd/gh/main.go` file into a `bin/gh` binary.
|
||||
2. `bin/gh issue list --limit 5` - Runs the newly built `bin/gh` binary (note: on Windows you must use
|
||||
backslashes like `bin\gh`) and passes the following arguments to the process: `["issue", "list", "--limit", "5"]`.
|
||||
3. `func main()` inside `cmd/gh/main.go` is the first Go function that runs. The arguments passed to the
|
||||
process are available through `os.Args`.
|
||||
4. The `main` package initializes the "root" command with `root.NewCmdRoot()` and dispatches execution to it
|
||||
with `rootCmd.ExecuteC()`.
|
||||
5. The [root command](../pkg/cmd/root/root.go) represents the top-level `gh` command and knows how to
|
||||
dispatch execution to any other gh command nested under it.
|
||||
6. Based on `["issue", "list"]` arguments, the execution reaches the `RunE` block of the `cobra.Command`
|
||||
within [pkg/cmd/issue/list/list.go](../pkg/cmd/issue/list/list.go).
|
||||
7. The `--limit 5` flag originally passed as arguments be automatically parsed and its value stored as
|
||||
`opts.LimitResults`.
|
||||
8. `func listRun()` is called, which is responsible for implementing the logic of the `gh issue list` command.
|
||||
9. The command collects information from sources like the GitHub API then writes the final output to
|
||||
standard output and standard error [streams](../pkg/iostreams/iostreams.go) available at `opts.IO`.
|
||||
10. The program execution is now back at `func main()` of `cmd/gh/main.go`. If there were any Go errors as a
|
||||
result of processing the command, the function will abort the process with a non-zero exit status.
|
||||
Otherwise, the process ends with status 0 indicating success.
|
||||
|
||||
## How to add a new command
|
||||
|
||||
0. First, check on our issue tracker to verify that our team had approved the plans for a new command.
|
||||
1. Create a package for the new command, e.g. for a new command `gh boom` create the following directory
|
||||
structure: `pkg/cmd/boom/`
|
||||
2. The new package should expose a method, e.g. `NewCmdBoom()`, that accepts a `*cmdutil.Factory` type and
|
||||
returns a `*cobra.Command`.
|
||||
* Any logic specific to this command should be kept within the command's package and not added to any
|
||||
"global" packages like `api` or `utils`.
|
||||
3. Use the method from the previous step to generate the command and add it to the command tree, typically
|
||||
somewhere in the `NewCmdRoot()` method.
|
||||
|
||||
## How to write tests
|
||||
|
||||
This task might be tricky. Typically, gh commands do things like look up information from the git repository
|
||||
in the current directory, query the GitHub API, scan the user's `~/.ssh/config` file, clone or fetch git
|
||||
repositories, etc. Naturally, none of these things should ever happen for real when running tests, unless
|
||||
you are sure that any filesystem operations are stricly scoped to a location made for and maintained by the
|
||||
test itself. To avoid actually running things like making real API requests or shelling out to `git`
|
||||
commands, we stub them. You should look at how that's done within some existing tests.
|
||||
|
||||
To make your code testable, write small, isolated pieces of functionality that are designed to be composed
|
||||
together. Prefer table-driven tests for maintaining variations of different test inputs and expectations
|
||||
when exercising a single piece of functionality.
|
||||
2
go.mod
2
go.mod
|
|
@ -27,7 +27,7 @@ require (
|
|||
github.com/rivo/uniseg v0.1.0
|
||||
github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b
|
||||
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f
|
||||
github.com/spf13/cobra v1.1.1
|
||||
github.com/spf13/cobra v1.1.3
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.6.1
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
|
||||
|
|
|
|||
6
go.sum
6
go.sum
|
|
@ -248,8 +248,8 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k
|
|||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4=
|
||||
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
|
||||
github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M=
|
||||
github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
|
|
@ -419,7 +419,7 @@ gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
|||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
|
|
@ -3,10 +3,8 @@ package config
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
|
|
@ -111,7 +109,7 @@ var ReadConfigFile = func(filename string) ([]byte, error) {
|
|||
}
|
||||
|
||||
var WriteConfigFile = func(filename string, data []byte) error {
|
||||
err := os.MkdirAll(path.Dir(filename), 0771)
|
||||
err := os.MkdirAll(filepath.Dir(filename), 0771)
|
||||
if err != nil {
|
||||
return pathError(err)
|
||||
}
|
||||
|
|
@ -122,11 +120,7 @@ var WriteConfigFile = func(filename string, data []byte) error {
|
|||
}
|
||||
defer cfgFile.Close()
|
||||
|
||||
n, err := cfgFile.Write(data)
|
||||
if err == nil && n < len(data) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
|
||||
_, err = cfgFile.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -263,7 +257,7 @@ func findRegularFile(p string) string {
|
|||
if s, err := os.Stat(p); err == nil && s.Mode().IsRegular() {
|
||||
return p
|
||||
}
|
||||
newPath := path.Dir(p)
|
||||
newPath := filepath.Dir(p)
|
||||
if newPath == p || newPath == "/" || newPath == "." {
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ package config
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -167,3 +169,28 @@ func Test_ConfigDir(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_configFile_Write_toDisk(t *testing.T) {
|
||||
configDir := filepath.Join(t.TempDir(), ".config", "gh")
|
||||
os.Setenv(GH_CONFIG_DIR, configDir)
|
||||
defer os.Unsetenv(GH_CONFIG_DIR)
|
||||
|
||||
cfg := NewFromString(`pager: less`)
|
||||
err := cfg.Write()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expectedConfig := "pager: less\n"
|
||||
if configBytes, err := ioutil.ReadFile(filepath.Join(configDir, "config.yml")); err != nil {
|
||||
t.Error(err)
|
||||
} else if string(configBytes) != expectedConfig {
|
||||
t.Errorf("expected config.yml %q, got %q", expectedConfig, string(configBytes))
|
||||
}
|
||||
|
||||
if configBytes, err := ioutil.ReadFile(filepath.Join(configDir, "hosts.yml")); err != nil {
|
||||
t.Error(err)
|
||||
} else if string(configBytes) != "" {
|
||||
t.Errorf("unexpected hosts.yml: %q", string(configBytes))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,11 +175,10 @@ func assertNextLineEquals(scanner *bufio.Scanner, expectedLine string) error {
|
|||
}
|
||||
|
||||
func BenchmarkGenManToFile(b *testing.B) {
|
||||
file, err := ioutil.TempFile("", "")
|
||||
file, err := ioutil.TempFile(b.TempDir(), "")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer os.Remove(file.Name())
|
||||
defer file.Close()
|
||||
|
||||
b.ResetTimer()
|
||||
|
|
|
|||
|
|
@ -83,11 +83,10 @@ func TestGenMdTree(t *testing.T) {
|
|||
}
|
||||
|
||||
func BenchmarkGenMarkdownToFile(b *testing.B) {
|
||||
file, err := ioutil.TempFile("", "")
|
||||
file, err := ioutil.TempFile(b.TempDir(), "")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer os.Remove(file.Name())
|
||||
defer file.Close()
|
||||
|
||||
b.ResetTimer()
|
||||
|
|
|
|||
|
|
@ -48,26 +48,26 @@ func actionsExplainer(cs *iostreams.ColorScheme) string {
|
|||
|
||||
GitHub CLI integrates with Actions to help you manage runs and workflows.
|
||||
|
||||
%s
|
||||
gh run list: List recent workflow runs
|
||||
gh run view: View details for a workflow run or one of its jobs
|
||||
gh run watch: Watch a workflow run while it executes
|
||||
gh run rerun: Rerun a failed workflow run
|
||||
%s
|
||||
gh run list: List recent workflow runs
|
||||
gh run view: View details for a workflow run or one of its jobs
|
||||
gh run watch: Watch a workflow run while it executes
|
||||
gh run rerun: Rerun a failed workflow run
|
||||
gh run download: Download artifacts generated by runs
|
||||
|
||||
To see more help, run 'gh help run <subcommand>'
|
||||
|
||||
%s
|
||||
gh workflow list: List all the workflow files in your repository
|
||||
gh workflow view: View details for a workflow file
|
||||
gh workflow enable: Enable a workflow file
|
||||
gh workflow disable: Disable a workflow file
|
||||
%s
|
||||
gh workflow list: List all the workflow files in your repository
|
||||
gh workflow view: View details for a workflow file
|
||||
gh workflow enable: Enable a workflow file
|
||||
gh workflow disable: Disable a workflow file
|
||||
gh workflow run: Trigger a workflow_dispatch run for a workflow file
|
||||
|
||||
To see more help, run 'gh help workflow <subcommand>'
|
||||
|
||||
For more in depth help including examples, see online documentation at:
|
||||
https://docs.github.com/en/actions/guides/managing-github-actions-with-github-cli
|
||||
<https://docs.github.com/en/actions/guides/managing-github-actions-with-github-cli>
|
||||
`, header, runHeader, workflowHeader)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -693,6 +693,9 @@ func Test_apiRun_inputFile(t *testing.T) {
|
|||
contentLength: 10,
|
||||
},
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
io, stdin, _, _ := iostreams.Test()
|
||||
|
|
@ -702,13 +705,12 @@ func Test_apiRun_inputFile(t *testing.T) {
|
|||
if tt.inputFile == "-" {
|
||||
_, _ = stdin.Write(tt.inputContents)
|
||||
} else {
|
||||
f, err := ioutil.TempFile("", tt.inputFile)
|
||||
f, err := ioutil.TempFile(tempDir, tt.inputFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _ = f.Write(tt.inputContents)
|
||||
f.Close()
|
||||
t.Cleanup(func() { os.Remove(f.Name()) })
|
||||
defer f.Close()
|
||||
inputFile = f.Name()
|
||||
}
|
||||
|
||||
|
|
@ -825,13 +827,13 @@ func Test_parseFields(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_magicFieldValue(t *testing.T) {
|
||||
f, err := ioutil.TempFile("", "gh-test")
|
||||
f, err := ioutil.TempFile(t.TempDir(), "gh-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fmt.Fprint(f, "file contents")
|
||||
f.Close()
|
||||
t.Cleanup(func() { os.Remove(f.Name()) })
|
||||
|
||||
io, _, _, _ := iostreams.Test()
|
||||
|
||||
|
|
@ -932,13 +934,13 @@ func Test_magicFieldValue(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_openUserFile(t *testing.T) {
|
||||
f, err := ioutil.TempFile("", "gh-test")
|
||||
f, err := ioutil.TempFile(t.TempDir(), "gh-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fmt.Fprint(f, "file contents")
|
||||
f.Close()
|
||||
t.Cleanup(func() { os.Remove(f.Name()) })
|
||||
|
||||
file, length, err := openUserFile(f.Name(), nil)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ func NewCmdCompletion(io *iostreams.IOStreams) *cobra.Command {
|
|||
|
||||
When installing GitHub CLI through a package manager, it's possible that
|
||||
no additional shell configuration is necessary to gain completion support. For
|
||||
Homebrew, see https://docs.brew.sh/Shell-Completion
|
||||
Homebrew, see <https://docs.brew.sh/Shell-Completion>
|
||||
|
||||
If you need to set up completions manually, follow the instructions below. The exact
|
||||
config file locations might vary based on your system. Make sure to restart your
|
||||
|
|
|
|||
|
|
@ -151,7 +151,13 @@ func createRun(opts *CreateOptions) error {
|
|||
var httpError api.HTTPError
|
||||
if errors.As(err, &httpError) {
|
||||
if httpError.OAuthScopes != "" && !strings.Contains(httpError.OAuthScopes, "gist") {
|
||||
return fmt.Errorf("This command requires the 'gist' OAuth scope.\nPlease re-authenticate by doing `gh config set -h github.com oauth_token ''` and running the command again.")
|
||||
return fmt.Errorf("This command requires the 'gist' OAuth scope.\nPlease re-authenticate with: gh auth refresh -h %s -s gist", host)
|
||||
}
|
||||
if httpError.StatusCode == http.StatusUnprocessableEntity {
|
||||
if detectEmptyFiles(files) {
|
||||
fmt.Fprintf(errOut, "%s Failed to create gist: %s\n", cs.FailureIcon(), "a gist file cannot be blank")
|
||||
return cmdutil.SilentError
|
||||
}
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%s Failed to create gist: %w", cs.Red("X"), err)
|
||||
|
|
@ -266,3 +272,12 @@ func createGist(client *http.Client, hostname, description string, public bool,
|
|||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func detectEmptyFiles(files map[string]*shared.GistFile) bool {
|
||||
for _, file := range files {
|
||||
if strings.TrimSpace(file.Content) == "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import (
|
|||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/run"
|
||||
"github.com/cli/cli/pkg/cmd/gist/shared"
|
||||
|
|
@ -18,10 +20,6 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
fixtureFile = "../fixture.txt"
|
||||
)
|
||||
|
||||
func Test_processFiles(t *testing.T) {
|
||||
fakeStdin := strings.NewReader("hey cool how is it going")
|
||||
files, err := processFiles(ioutil.NopCloser(fakeStdin), "", []string{"-"})
|
||||
|
|
@ -164,15 +162,22 @@ func TestNewCmdCreate(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_createRun(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
fixtureFile := path.Join(tempDir, "fixture.txt")
|
||||
assert.NoError(t, ioutil.WriteFile(fixtureFile, []byte("{}"), 0644))
|
||||
emptyFile := path.Join(tempDir, "empty.txt")
|
||||
assert.NoError(t, ioutil.WriteFile(emptyFile, []byte(" \t\n"), 0644))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
opts *CreateOptions
|
||||
stdin string
|
||||
wantOut string
|
||||
wantStderr string
|
||||
wantParams map[string]interface{}
|
||||
wantErr bool
|
||||
wantBrowse string
|
||||
name string
|
||||
opts *CreateOptions
|
||||
stdin string
|
||||
wantOut string
|
||||
wantStderr string
|
||||
wantParams map[string]interface{}
|
||||
wantErr bool
|
||||
wantBrowse string
|
||||
responseStatus int
|
||||
}{
|
||||
{
|
||||
name: "public",
|
||||
|
|
@ -193,6 +198,7 @@ func Test_createRun(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
responseStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "with description",
|
||||
|
|
@ -213,6 +219,7 @@ func Test_createRun(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
responseStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "multiple files",
|
||||
|
|
@ -236,6 +243,28 @@ func Test_createRun(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
responseStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "file with empty content",
|
||||
opts: &CreateOptions{
|
||||
Filenames: []string{emptyFile},
|
||||
},
|
||||
wantOut: "",
|
||||
wantStderr: heredoc.Doc(`
|
||||
- Creating gist empty.txt
|
||||
X Failed to create gist: a gist file cannot be blank
|
||||
`),
|
||||
wantErr: true,
|
||||
wantParams: map[string]interface{}{
|
||||
"description": "",
|
||||
"updated_at": "0001-01-01T00:00:00Z",
|
||||
"public": false,
|
||||
"files": map[string]interface{}{
|
||||
"empty.txt": map[string]interface{}{"content": " \t\n"},
|
||||
},
|
||||
},
|
||||
responseStatus: http.StatusUnprocessableEntity,
|
||||
},
|
||||
{
|
||||
name: "stdin arg",
|
||||
|
|
@ -256,6 +285,7 @@ func Test_createRun(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
responseStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "web arg",
|
||||
|
|
@ -277,14 +307,22 @@ func Test_createRun(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
responseStatus: http.StatusOK,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
reg := &httpmock.Registry{}
|
||||
reg.Register(httpmock.REST("POST", "gists"),
|
||||
httpmock.JSONResponse(struct {
|
||||
Html_url string
|
||||
}{"https://gist.github.com/aa5a315d61ae9438b18d"}))
|
||||
if tt.responseStatus == http.StatusOK {
|
||||
reg.Register(
|
||||
httpmock.REST("POST", "gists"),
|
||||
httpmock.StringResponse(`{
|
||||
"html_url": "https://gist.github.com/aa5a315d61ae9438b18d"
|
||||
}`))
|
||||
} else {
|
||||
reg.Register(
|
||||
httpmock.REST("POST", "gists"),
|
||||
httpmock.StatusStringResponse(tt.responseStatus, "{}"))
|
||||
}
|
||||
|
||||
mockClient := func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
|
|
@ -325,6 +363,32 @@ func Test_createRun(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_detectEmptyFiles(t *testing.T) {
|
||||
tests := []struct {
|
||||
content string
|
||||
isEmptyFile bool
|
||||
}{
|
||||
{
|
||||
content: "{}",
|
||||
isEmptyFile: false,
|
||||
},
|
||||
{
|
||||
content: "\n\t",
|
||||
isEmptyFile: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
files := map[string]*shared.GistFile{}
|
||||
files["file"] = &shared.GistFile{
|
||||
Content: tt.content,
|
||||
}
|
||||
|
||||
isEmptyFile := detectEmptyFiles(files)
|
||||
assert.Equal(t, tt.isEmptyFile, isEmptyFile)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CreateRun_reauth(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
reg.Register(httpmock.REST("POST", "gists"), func(req *http.Request) (*http.Response, error) {
|
||||
|
|
@ -332,33 +396,24 @@ func Test_CreateRun_reauth(t *testing.T) {
|
|||
StatusCode: 404,
|
||||
Request: req,
|
||||
Header: map[string][]string{
|
||||
"X-Oauth-Scopes": {"coolScope"},
|
||||
"X-Oauth-Scopes": {"repo, read:org"},
|
||||
},
|
||||
Body: ioutil.NopCloser(bytes.NewBufferString("oh no")),
|
||||
}, nil
|
||||
})
|
||||
|
||||
mockClient := func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
|
||||
io, _, _, _ := iostreams.Test()
|
||||
|
||||
opts := &CreateOptions{
|
||||
IO: io,
|
||||
HttpClient: mockClient,
|
||||
IO: io,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
},
|
||||
Config: func() (config.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
Filenames: []string{fixtureFile},
|
||||
}
|
||||
|
||||
err := createRun(opts)
|
||||
if err == nil {
|
||||
t.Fatalf("expected oauth error")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "Please re-authenticate") {
|
||||
t.Errorf("got unexpected error: %s", err)
|
||||
}
|
||||
assert.EqualError(t, err, "This command requires the 'gist' OAuth scope.\nPlease re-authenticate with: gh auth refresh -h github.com -s gist")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
{}
|
||||
|
|
@ -20,7 +20,7 @@ type GistFile struct {
|
|||
Filename string `json:"filename,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Language string `json:"language,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type GistOwner struct {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
|
@ -422,8 +421,9 @@ func TestIssueCreate_recover(t *testing.T) {
|
|||
},
|
||||
})
|
||||
|
||||
tmpfile, err := ioutil.TempFile(os.TempDir(), "testrecover*")
|
||||
tmpfile, err := ioutil.TempFile(t.TempDir(), "testrecover*")
|
||||
assert.NoError(t, err)
|
||||
defer tmpfile.Close()
|
||||
|
||||
state := prShared.IssueMetadataState{
|
||||
Title: "recovered title",
|
||||
|
|
|
|||
|
|
@ -149,7 +149,9 @@ func editRun(opts *EditOptions) error {
|
|||
editable.Assignees.Default = issue.Assignees.Logins()
|
||||
editable.Labels.Default = issue.Labels.Names()
|
||||
editable.Projects.Default = issue.ProjectCards.ProjectNames()
|
||||
editable.Milestone.Default = issue.Milestone.Title
|
||||
if issue.Milestone != nil {
|
||||
editable.Milestone.Default = issue.Milestone.Title
|
||||
}
|
||||
|
||||
if opts.Interactive {
|
||||
err = opts.FieldsToEditSurvey(&editable)
|
||||
|
|
|
|||
|
|
@ -155,8 +155,7 @@ func listRun(opts *ListOptions) error {
|
|||
defer opts.IO.StopPager()
|
||||
|
||||
if opts.Exporter != nil {
|
||||
data := api.ExportIssues(listResult.Issues, opts.Exporter.Fields())
|
||||
return opts.Exporter.Write(opts.IO.Out, data, opts.IO.ColorEnabled())
|
||||
return opts.Exporter.Write(opts.IO.Out, listResult.Issues, opts.IO.ColorEnabled())
|
||||
}
|
||||
|
||||
if isTerminal {
|
||||
|
|
|
|||
|
|
@ -96,11 +96,11 @@ func statusRun(opts *StatusOptions) error {
|
|||
|
||||
if opts.Exporter != nil {
|
||||
data := map[string]interface{}{
|
||||
"createdBy": api.ExportIssues(issuePayload.Authored.Issues, opts.Exporter.Fields()),
|
||||
"assigned": api.ExportIssues(issuePayload.Assigned.Issues, opts.Exporter.Fields()),
|
||||
"mentioned": api.ExportIssues(issuePayload.Mentioned.Issues, opts.Exporter.Fields()),
|
||||
"createdBy": issuePayload.Authored.Issues,
|
||||
"assigned": issuePayload.Assigned.Issues,
|
||||
"mentioned": issuePayload.Mentioned.Issues,
|
||||
}
|
||||
return opts.Exporter.Write(opts.IO.Out, &data, opts.IO.ColorEnabled())
|
||||
return opts.Exporter.Write(opts.IO.Out, data, opts.IO.ColorEnabled())
|
||||
}
|
||||
|
||||
out := opts.IO.Out
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"data": {
|
||||
"repository": {
|
||||
"issue": {
|
||||
"node": {
|
||||
"comments": {
|
||||
"nodes": [
|
||||
{
|
||||
|
|
@ -315,6 +314,5 @@
|
|||
"totalCount": 6
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
50
pkg/cmd/issue/view/http.go
Normal file
50
pkg/cmd/issue/view/http.go
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/ghinstance"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/shurcooL/graphql"
|
||||
)
|
||||
|
||||
func preloadIssueComments(client *http.Client, repo ghrepo.Interface, issue *api.Issue) error {
|
||||
type response struct {
|
||||
Node struct {
|
||||
Issue struct {
|
||||
Comments api.Comments `graphql:"comments(first: 100, after: $endCursor)"`
|
||||
} `graphql:"...on Issue"`
|
||||
} `graphql:"node(id: $id)"`
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"id": githubv4.ID(issue.ID),
|
||||
"endCursor": (*githubv4.String)(nil),
|
||||
}
|
||||
if issue.Comments.PageInfo.HasNextPage {
|
||||
variables["endCursor"] = githubv4.String(issue.Comments.PageInfo.EndCursor)
|
||||
} else {
|
||||
issue.Comments.Nodes = issue.Comments.Nodes[0:0]
|
||||
}
|
||||
|
||||
gql := graphql.NewClient(ghinstance.GraphQLEndpoint(repo.RepoHost()), client)
|
||||
for {
|
||||
var query response
|
||||
err := gql.QueryNamed(context.Background(), "CommentsForIssue", &query, variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
issue.Comments.Nodes = append(issue.Comments.Nodes, query.Node.Issue.Comments.Nodes...)
|
||||
if !query.Node.Issue.Comments.PageInfo.HasNextPage {
|
||||
break
|
||||
}
|
||||
variables["endCursor"] = githubv4.String(query.Node.Issue.Comments.PageInfo.EndCursor)
|
||||
}
|
||||
|
||||
issue.Comments.PageInfo.HasNextPage = false
|
||||
return nil
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/pkg/markdown"
|
||||
"github.com/cli/cli/pkg/set"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
|
@ -82,9 +83,17 @@ func viewRun(opts *ViewOptions) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
issue, repo, err := issueShared.IssueFromArg(apiClient, opts.BaseRepo, opts.SelectorArg)
|
||||
loadComments := opts.Comments
|
||||
if !loadComments && opts.Exporter != nil {
|
||||
fields := set.NewStringSet()
|
||||
fields.AddValues(opts.Exporter.Fields())
|
||||
loadComments = fields.Contains("comments")
|
||||
}
|
||||
|
||||
opts.IO.StartProgressIndicator()
|
||||
issue, err := findIssue(httpClient, opts.BaseRepo, opts.SelectorArg, loadComments)
|
||||
opts.IO.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -97,27 +106,14 @@ func viewRun(opts *ViewOptions) error {
|
|||
return opts.Browser.Browse(openURL)
|
||||
}
|
||||
|
||||
if opts.Comments {
|
||||
opts.IO.StartProgressIndicator()
|
||||
comments, err := api.CommentsForIssue(apiClient, repo, issue)
|
||||
opts.IO.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
issue.Comments = *comments
|
||||
}
|
||||
|
||||
opts.IO.DetectTerminalTheme()
|
||||
|
||||
err = opts.IO.StartPager()
|
||||
if err != nil {
|
||||
return err
|
||||
if err := opts.IO.StartPager(); err != nil {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "error starting pager: %v\n", err)
|
||||
}
|
||||
defer opts.IO.StopPager()
|
||||
|
||||
if opts.Exporter != nil {
|
||||
exportIssue := issue.ExportData(opts.Exporter.Fields())
|
||||
return opts.Exporter.Write(opts.IO.Out, exportIssue, opts.IO.ColorEnabled())
|
||||
return opts.Exporter.Write(opts.IO.Out, issue, opts.IO.ColorEnabled())
|
||||
}
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
|
|
@ -132,6 +128,19 @@ func viewRun(opts *ViewOptions) error {
|
|||
return printRawIssuePreview(opts.IO.Out, issue)
|
||||
}
|
||||
|
||||
func findIssue(client *http.Client, baseRepoFn func() (ghrepo.Interface, error), selector string, loadComments bool) (*api.Issue, error) {
|
||||
apiClient := api.NewClientFromHTTP(client)
|
||||
issue, repo, err := issueShared.IssueFromArg(apiClient, baseRepoFn, selector)
|
||||
if err != nil {
|
||||
return issue, err
|
||||
}
|
||||
|
||||
if loadComments {
|
||||
err = preloadIssueComments(client, repo, issue)
|
||||
}
|
||||
return issue, err
|
||||
}
|
||||
|
||||
func printRawIssuePreview(out io.Writer, issue *api.Issue) error {
|
||||
assignees := issueAssigneeList(*issue)
|
||||
labels := shared.IssueLabelList(*issue)
|
||||
|
|
@ -146,7 +155,11 @@ func printRawIssuePreview(out io.Writer, issue *api.Issue) error {
|
|||
fmt.Fprintf(out, "comments:\t%d\n", issue.Comments.TotalCount)
|
||||
fmt.Fprintf(out, "assignees:\t%s\n", assignees)
|
||||
fmt.Fprintf(out, "projects:\t%s\n", projects)
|
||||
fmt.Fprintf(out, "milestone:\t%s\n", issue.Milestone.Title)
|
||||
var milestoneTitle string
|
||||
if issue.Milestone != nil {
|
||||
milestoneTitle = issue.Milestone.Title
|
||||
}
|
||||
fmt.Fprintf(out, "milestone:\t%s\n", milestoneTitle)
|
||||
fmt.Fprintln(out, "--")
|
||||
fmt.Fprintln(out, issue.Body)
|
||||
return nil
|
||||
|
|
@ -187,7 +200,7 @@ func printHumanIssuePreview(opts *ViewOptions, issue *api.Issue) error {
|
|||
fmt.Fprint(out, cs.Bold("Projects: "))
|
||||
fmt.Fprintln(out, projects)
|
||||
}
|
||||
if issue.Milestone.Title != "" {
|
||||
if issue.Milestone != nil {
|
||||
fmt.Fprint(out, cs.Bold("Milestone: "))
|
||||
fmt.Fprintln(out, issue.Milestone.Title)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,10 +24,11 @@ type CheckoutOptions struct {
|
|||
HttpClient func() (*http.Client, error)
|
||||
Config func() (config.Config, error)
|
||||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Remotes func() (context.Remotes, error)
|
||||
Branch func() (string, error)
|
||||
|
||||
Finder shared.PRFinder
|
||||
|
||||
SelectorArg string
|
||||
RecurseSubmodules bool
|
||||
Force bool
|
||||
|
|
@ -48,8 +49,7 @@ func NewCmdCheckout(f *cmdutil.Factory, runF func(*CheckoutOptions) error) *cobr
|
|||
Short: "Check out a pull request in git",
|
||||
Args: cmdutil.ExactArgs(1, "argument required"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
opts.Finder = shared.NewFinder(f)
|
||||
|
||||
if len(args) > 0 {
|
||||
opts.SelectorArg = args[0]
|
||||
|
|
@ -70,18 +70,11 @@ func NewCmdCheckout(f *cmdutil.Factory, runF func(*CheckoutOptions) error) *cobr
|
|||
}
|
||||
|
||||
func checkoutRun(opts *CheckoutOptions) error {
|
||||
remotes, err := opts.Remotes()
|
||||
if err != nil {
|
||||
return err
|
||||
findOptions := shared.FindOptions{
|
||||
Selector: opts.SelectorArg,
|
||||
Fields: []string{"number", "headRefName", "headRepository", "headRepositoryOwner", "isCrossRepository"},
|
||||
}
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
pr, baseRepo, err := shared.PRFromArgs(apiClient, opts.BaseRepo, opts.Branch, opts.Remotes, opts.SelectorArg)
|
||||
pr, baseRepo, err := opts.Finder.Find(findOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -90,8 +83,12 @@ func checkoutRun(opts *CheckoutOptions) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
protocol, _ := cfg.Get(baseRepo.RepoHost(), "git_protocol")
|
||||
|
||||
remotes, err := opts.Remotes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
baseRemote, _ := remotes.FindByRepo(baseRepo.RepoOwner(), baseRepo.RepoName())
|
||||
baseURLOrName := ghrepo.FormatRemoteURL(baseRepo, protocol)
|
||||
if baseRemote != nil {
|
||||
|
|
@ -112,6 +109,12 @@ func checkoutRun(opts *CheckoutOptions) error {
|
|||
if headRemote != nil {
|
||||
cmdQueue = append(cmdQueue, cmdsForExistingRemote(headRemote, pr, opts)...)
|
||||
} else {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
defaultBranch, err := api.RepoDefaultBranch(apiClient, baseRepo)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ package checkout
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
|
|
@ -13,6 +13,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/cmd/pr/shared"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
|
|
@ -21,6 +22,43 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// repo: either "baseOwner/baseRepo" or "baseOwner/baseRepo:defaultBranch"
|
||||
// prHead: "headOwner/headRepo:headBranch"
|
||||
func stubPR(repo, prHead string) (ghrepo.Interface, *api.PullRequest) {
|
||||
defaultBranch := ""
|
||||
if idx := strings.IndexRune(repo, ':'); idx >= 0 {
|
||||
defaultBranch = repo[idx+1:]
|
||||
repo = repo[:idx]
|
||||
}
|
||||
baseRepo, err := ghrepo.FromFullName(repo)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if defaultBranch != "" {
|
||||
baseRepo = api.InitRepoHostname(&api.Repository{
|
||||
Name: baseRepo.RepoName(),
|
||||
Owner: api.RepositoryOwner{Login: baseRepo.RepoOwner()},
|
||||
DefaultBranchRef: api.BranchRef{Name: defaultBranch},
|
||||
}, baseRepo.RepoHost())
|
||||
}
|
||||
|
||||
idx := strings.IndexRune(prHead, ':')
|
||||
headRefName := prHead[idx+1:]
|
||||
headRepo, err := ghrepo.FromFullName(prHead[:idx])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return baseRepo, &api.PullRequest{
|
||||
Number: 123,
|
||||
HeadRefName: headRefName,
|
||||
HeadRepositoryOwner: api.Owner{Login: headRepo.RepoOwner()},
|
||||
HeadRepository: &api.PRRepository{Name: headRepo.RepoName()},
|
||||
IsCrossRepository: !ghrepo.IsSame(baseRepo, headRepo),
|
||||
MaintainerCanModify: false,
|
||||
}
|
||||
}
|
||||
|
||||
func runCommand(rt http.RoundTripper, remotes context.Remotes, branch string, cli string) (*test.CmdOut, error) {
|
||||
io, _, stdout, stderr := iostreams.Test()
|
||||
|
||||
|
|
@ -32,13 +70,6 @@ func runCommand(rt http.RoundTripper, remotes context.Remotes, branch string, cl
|
|||
Config: func() (config.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return api.InitRepoHostname(&api.Repository{
|
||||
Name: "REPO",
|
||||
Owner: api.RepositoryOwner{Login: "OWNER"},
|
||||
DefaultBranchRef: api.BranchRef{Name: "master"},
|
||||
}, "github.com"), nil
|
||||
},
|
||||
Remotes: func() (context.Remotes, error) {
|
||||
if remotes == nil {
|
||||
return context.Remotes{
|
||||
|
|
@ -78,20 +109,9 @@ func TestPRCheckout_sameRepo(t *testing.T) {
|
|||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
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
|
||||
} } } }
|
||||
`))
|
||||
baseRepo, pr := stubPR("OWNER/REPO", "OWNER/REPO:feature")
|
||||
finder := shared.RunCommandFinder("123", pr, baseRepo)
|
||||
finder.ExpectFields([]string{"number", "headRefName", "headRepository", "headRepositoryOwner", "isCrossRepository"})
|
||||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
|
@ -103,141 +123,17 @@ func TestPRCheckout_sameRepo(t *testing.T) {
|
|||
cs.Register(`git config branch\.feature\.merge refs/heads/feature`, 0, "")
|
||||
|
||||
output, err := runCommand(http, nil, "master", `123`)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
}
|
||||
|
||||
func TestPRCheckout_urlArg(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
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
|
||||
} } } }
|
||||
`))
|
||||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature`, 0, "")
|
||||
cs.Register(`git show-ref --verify -- refs/heads/feature`, 1, "")
|
||||
cs.Register(`git checkout -b feature --no-track origin/feature`, 0, "")
|
||||
cs.Register(`git config branch\.feature\.remote origin`, 0, "")
|
||||
cs.Register(`git config branch\.feature\.merge refs/heads/feature`, 0, "")
|
||||
|
||||
output, err := runCommand(http, nil, "master", `https://github.com/OWNER/REPO/pull/123/files`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
}
|
||||
|
||||
func TestPRCheckout_urlArg_differentBase(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
http.Register(httpmock.GraphQL(`query PullRequestByNumber\b`), httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"number": 123,
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "POE"
|
||||
},
|
||||
"isCrossRepository": false,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
http.StubRepoInfoResponse("OWNER", "REPO", "master")
|
||||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
cs.Register(`git fetch https://github\.com/OTHER/POE\.git refs/pull/123/head:feature`, 0, "")
|
||||
cs.Register(`git config branch\.feature\.merge`, 1, "")
|
||||
cs.Register(`git checkout feature`, 0, "")
|
||||
cs.Register(`git config branch\.feature\.remote https://github\.com/OTHER/POE\.git`, 0, "")
|
||||
cs.Register(`git config branch\.feature\.merge refs/pull/123/head`, 0, "")
|
||||
|
||||
output, err := runCommand(http, nil, "master", `https://github.com/OTHER/POE/pull/123/files`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
bodyBytes, _ := ioutil.ReadAll(http.Requests[0].Body)
|
||||
reqBody := struct {
|
||||
Variables struct {
|
||||
Owner string
|
||||
Repo string
|
||||
}
|
||||
}{}
|
||||
_ = json.Unmarshal(bodyBytes, &reqBody)
|
||||
|
||||
assert.Equal(t, "OTHER", reqBody.Variables.Owner)
|
||||
assert.Equal(t, "POE", reqBody.Variables.Repo)
|
||||
}
|
||||
|
||||
func TestPRCheckout_branchArg(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(httpmock.GraphQL(`query PullRequestForBranch\b`), httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [
|
||||
{ "number": 123,
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO"
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": false }
|
||||
] } } } }
|
||||
`))
|
||||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
cs.Register(`git fetch origin refs/pull/123/head:feature`, 0, "")
|
||||
cs.Register(`git config branch\.feature\.merge`, 1, "")
|
||||
cs.Register(`git checkout feature`, 0, "")
|
||||
cs.Register(`git config branch\.feature\.remote origin`, 0, "")
|
||||
cs.Register(`git config branch\.feature\.merge refs/pull/123/head`, 0, "")
|
||||
|
||||
output, err := runCommand(http, nil, "master", `hubot:feature`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
}
|
||||
|
||||
func TestPRCheckout_existingBranch(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
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
|
||||
} } } }
|
||||
`))
|
||||
baseRepo, pr := stubPR("OWNER/REPO", "OWNER/REPO:feature")
|
||||
shared.RunCommandFinder("123", pr, baseRepo)
|
||||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
|
@ -250,6 +146,7 @@ func TestPRCheckout_existingBranch(t *testing.T) {
|
|||
output, err := runCommand(http, nil, "master", `123`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
}
|
||||
|
||||
func TestPRCheckout_differentRepo_remoteExists(t *testing.T) {
|
||||
|
|
@ -267,20 +164,9 @@ func TestPRCheckout_differentRepo_remoteExists(t *testing.T) {
|
|||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(httpmock.GraphQL(`query PullRequestByNumber\b`), httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"number": 123,
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO"
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
baseRepo, pr := stubPR("OWNER/REPO", "hubot/REPO:feature")
|
||||
finder := shared.RunCommandFinder("123", pr, baseRepo)
|
||||
finder.ExpectFields([]string{"number", "headRefName", "headRepository", "headRepositoryOwner", "isCrossRepository"})
|
||||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
|
@ -294,26 +180,16 @@ func TestPRCheckout_differentRepo_remoteExists(t *testing.T) {
|
|||
output, err := runCommand(http, remotes, "master", `123`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
}
|
||||
|
||||
func TestPRCheckout_differentRepo(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(httpmock.GraphQL(`query PullRequestByNumber\b`), httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"number": 123,
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO"
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
baseRepo, pr := stubPR("OWNER/REPO:master", "hubot/REPO:feature")
|
||||
finder := shared.RunCommandFinder("123", pr, baseRepo)
|
||||
finder.ExpectFields([]string{"number", "headRefName", "headRepository", "headRepositoryOwner", "isCrossRepository"})
|
||||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
|
@ -327,26 +203,15 @@ func TestPRCheckout_differentRepo(t *testing.T) {
|
|||
output, err := runCommand(http, nil, "master", `123`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
}
|
||||
|
||||
func TestPRCheckout_differentRepo_existingBranch(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(httpmock.GraphQL(`query PullRequestByNumber\b`), httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"number": 123,
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO"
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
baseRepo, pr := stubPR("OWNER/REPO:master", "hubot/REPO:feature")
|
||||
shared.RunCommandFinder("123", pr, baseRepo)
|
||||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
|
@ -358,26 +223,15 @@ func TestPRCheckout_differentRepo_existingBranch(t *testing.T) {
|
|||
output, err := runCommand(http, nil, "master", `123`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
}
|
||||
|
||||
func TestPRCheckout_detachedHead(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(httpmock.GraphQL(`query PullRequestByNumber\b`), httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"number": 123,
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO"
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": true
|
||||
} } } }
|
||||
`))
|
||||
baseRepo, pr := stubPR("OWNER/REPO:master", "hubot/REPO:feature")
|
||||
shared.RunCommandFinder("123", pr, baseRepo)
|
||||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
|
@ -389,26 +243,15 @@ func TestPRCheckout_detachedHead(t *testing.T) {
|
|||
output, err := runCommand(http, nil, "", `123`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
}
|
||||
|
||||
func TestPRCheckout_differentRepo_currentBranch(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(httpmock.GraphQL(`query PullRequestByNumber\b`), httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"number": 123,
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO"
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
baseRepo, pr := stubPR("OWNER/REPO:master", "hubot/REPO:feature")
|
||||
shared.RunCommandFinder("123", pr, baseRepo)
|
||||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
|
@ -420,26 +263,15 @@ func TestPRCheckout_differentRepo_currentBranch(t *testing.T) {
|
|||
output, err := runCommand(http, nil, "feature", `123`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
}
|
||||
|
||||
func TestPRCheckout_differentRepo_invalidBranchName(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(httpmock.GraphQL(`query PullRequestByNumber\b`), httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"number": 123,
|
||||
"headRefName": "-foo",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO"
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
baseRepo, pr := stubPR("OWNER/REPO", "hubot/REPO:-foo")
|
||||
shared.RunCommandFinder("123", pr, baseRepo)
|
||||
|
||||
_, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
|
@ -447,26 +279,16 @@ func TestPRCheckout_differentRepo_invalidBranchName(t *testing.T) {
|
|||
output, err := runCommand(http, nil, "master", `123`)
|
||||
assert.EqualError(t, err, `invalid branch name: "-foo"`)
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
}
|
||||
|
||||
func TestPRCheckout_maintainerCanModify(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(httpmock.GraphQL(`query PullRequestByNumber\b`), httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"number": 123,
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO"
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": true
|
||||
} } } }
|
||||
`))
|
||||
baseRepo, pr := stubPR("OWNER/REPO:master", "hubot/REPO:feature")
|
||||
pr.MaintainerCanModify = true
|
||||
shared.RunCommandFinder("123", pr, baseRepo)
|
||||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
|
@ -480,25 +302,14 @@ func TestPRCheckout_maintainerCanModify(t *testing.T) {
|
|||
output, err := runCommand(http, nil, "master", `123`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
}
|
||||
|
||||
func TestPRCheckout_recurseSubmodules(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
|
||||
} } } }
|
||||
`))
|
||||
baseRepo, pr := stubPR("OWNER/REPO", "OWNER/REPO:feature")
|
||||
shared.RunCommandFinder("123", pr, baseRepo)
|
||||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
|
@ -513,25 +324,14 @@ func TestPRCheckout_recurseSubmodules(t *testing.T) {
|
|||
output, err := runCommand(http, nil, "master", `123 --recurse-submodules`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
}
|
||||
|
||||
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
|
||||
} } } }
|
||||
`))
|
||||
baseRepo, pr := stubPR("OWNER/REPO", "OWNER/REPO:feature")
|
||||
shared.RunCommandFinder("123", pr, baseRepo)
|
||||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
|
@ -545,26 +345,15 @@ func TestPRCheckout_force(t *testing.T) {
|
|||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
}
|
||||
|
||||
func TestPRCheckout_detach(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(httpmock.GraphQL(`query PullRequestByNumber\b`), httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"number": 123,
|
||||
"headRef": "f8f8f8",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO"
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": true
|
||||
} } } }
|
||||
`))
|
||||
baseRepo, pr := stubPR("OWNER/REPO:master", "hubot/REPO:feature")
|
||||
shared.RunCommandFinder("123", pr, baseRepo)
|
||||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
|
@ -573,6 +362,7 @@ func TestPRCheckout_detach(t *testing.T) {
|
|||
cs.Register(`git fetch origin refs/pull/123/head`, 0, "")
|
||||
|
||||
output, err := runCommand(http, nil, "", `123 --detach`)
|
||||
assert.Nil(t, err)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,10 @@ package checks
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
|
|
@ -23,26 +20,19 @@ type browser interface {
|
|||
}
|
||||
|
||||
type ChecksOptions struct {
|
||||
HttpClient func() (*http.Client, error)
|
||||
IO *iostreams.IOStreams
|
||||
Browser browser
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Branch func() (string, error)
|
||||
Remotes func() (context.Remotes, error)
|
||||
IO *iostreams.IOStreams
|
||||
Browser browser
|
||||
|
||||
WebMode bool
|
||||
Finder shared.PRFinder
|
||||
|
||||
SelectorArg string
|
||||
WebMode bool
|
||||
}
|
||||
|
||||
func NewCmdChecks(f *cmdutil.Factory, runF func(*ChecksOptions) error) *cobra.Command {
|
||||
opts := &ChecksOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
Branch: f.Branch,
|
||||
Remotes: f.Remotes,
|
||||
BaseRepo: f.BaseRepo,
|
||||
Browser: f.Browser,
|
||||
IO: f.IOStreams,
|
||||
Browser: f.Browser,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
|
@ -56,8 +46,7 @@ func NewCmdChecks(f *cmdutil.Factory, runF func(*ChecksOptions) error) *cobra.Co
|
|||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
opts.Finder = shared.NewFinder(f)
|
||||
|
||||
if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 {
|
||||
return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")}
|
||||
|
|
@ -81,25 +70,17 @@ func NewCmdChecks(f *cmdutil.Factory, runF func(*ChecksOptions) error) *cobra.Co
|
|||
}
|
||||
|
||||
func checksRun(opts *ChecksOptions) error {
|
||||
httpClient, err := opts.HttpClient()
|
||||
findOptions := shared.FindOptions{
|
||||
Selector: opts.SelectorArg,
|
||||
Fields: []string{"number", "baseRefName", "statusCheckRollup"},
|
||||
}
|
||||
if opts.WebMode {
|
||||
findOptions.Fields = []string{"number"}
|
||||
}
|
||||
pr, baseRepo, err := opts.Finder.Find(findOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
pr, baseRepo, err := shared.PRFromArgs(apiClient, opts.BaseRepo, opts.Branch, opts.Remotes, opts.SelectorArg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(pr.Commits.Nodes) == 0 {
|
||||
return fmt.Errorf("no commit found on the pull request")
|
||||
}
|
||||
|
||||
rollup := pr.Commits.Nodes[0].Commit.StatusCheckRollup.Contexts.Nodes
|
||||
if len(rollup) == 0 {
|
||||
return fmt.Errorf("no checks reported on the '%s' branch", pr.BaseRefName)
|
||||
}
|
||||
|
||||
isTerminal := opts.IO.IsStdoutTTY()
|
||||
|
||||
|
|
@ -111,6 +92,15 @@ func checksRun(opts *ChecksOptions) error {
|
|||
return opts.Browser.Browse(openURL)
|
||||
}
|
||||
|
||||
if len(pr.StatusCheckRollup.Nodes) == 0 {
|
||||
return fmt.Errorf("no commit found on the pull request")
|
||||
}
|
||||
|
||||
rollup := pr.StatusCheckRollup.Nodes[0].Commit.StatusCheckRollup.Contexts.Nodes
|
||||
if len(rollup) == 0 {
|
||||
return fmt.Errorf("no checks reported on the '%s' branch", pr.BaseRefName)
|
||||
}
|
||||
|
||||
passing := 0
|
||||
failing := 0
|
||||
pending := 0
|
||||
|
|
@ -128,7 +118,7 @@ func checksRun(opts *ChecksOptions) error {
|
|||
|
||||
outputs := []output{}
|
||||
|
||||
for _, c := range pr.Commits.Nodes[0].Commit.StatusCheckRollup.Contexts.Nodes {
|
||||
for _, c := range pr.StatusCheckRollup.Nodes[0].Commit.StatusCheckRollup.Contexts.Nodes {
|
||||
mark := "✓"
|
||||
bucket := "pass"
|
||||
state := c.State
|
||||
|
|
|
|||
|
|
@ -2,16 +2,20 @@ package checks
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/internal/run"
|
||||
"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"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewCmdChecks(t *testing.T) {
|
||||
|
|
@ -66,36 +70,20 @@ func Test_checksRun(t *testing.T) {
|
|||
tests := []struct {
|
||||
name string
|
||||
fixture string
|
||||
stubs func(*httpmock.Registry)
|
||||
prJSON string
|
||||
nontty bool
|
||||
wantOut string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "no commits",
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"pullRequest": { "number": 123 }
|
||||
} } }
|
||||
`))
|
||||
},
|
||||
name: "no commits",
|
||||
prJSON: `{ "number": 123 }`,
|
||||
wantOut: "",
|
||||
wantErr: "no commit found on the pull request",
|
||||
},
|
||||
{
|
||||
name: "no checks",
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"pullRequest": { "number": 123, "commits": { "nodes": [{"commit": {"oid": "abc"}}]}, "baseRefName": "master" }
|
||||
} } }
|
||||
`))
|
||||
},
|
||||
name: "no checks",
|
||||
prJSON: `{ "number": 123, "statusCheckRollup": { "nodes": [{"commit": {"oid": "abc"}}]}, "baseRefName": "master" }`,
|
||||
wantOut: "",
|
||||
wantErr: "no checks reported on the 'master' branch",
|
||||
},
|
||||
|
|
@ -124,17 +112,9 @@ func Test_checksRun(t *testing.T) {
|
|||
wantErr: "SilentError",
|
||||
},
|
||||
{
|
||||
name: "no checks",
|
||||
nontty: true,
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"pullRequest": { "number": 123, "commits": { "nodes": [{"commit": {"oid": "abc"}}]}, "baseRefName": "master" }
|
||||
} } }
|
||||
`))
|
||||
},
|
||||
name: "no checks",
|
||||
nontty: true,
|
||||
prJSON: `{ "number": 123, "statusCheckRollup": { "nodes": [{"commit": {"oid": "abc"}}]}, "baseRefName": "master" }`,
|
||||
wantOut: "",
|
||||
wantErr: "no checks reported on the 'master' branch",
|
||||
},
|
||||
|
|
@ -170,28 +150,26 @@ func Test_checksRun(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
io, _, stdout, _ := iostreams.Test()
|
||||
io.SetStdoutTTY(!tt.nontty)
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(!tt.nontty)
|
||||
|
||||
var response *api.PullRequest
|
||||
var jsonReader io.Reader
|
||||
if tt.fixture != "" {
|
||||
ff, err := os.Open(tt.fixture)
|
||||
require.NoError(t, err)
|
||||
defer ff.Close()
|
||||
jsonReader = ff
|
||||
} else {
|
||||
jsonReader = bytes.NewBufferString(tt.prJSON)
|
||||
}
|
||||
dec := json.NewDecoder(jsonReader)
|
||||
require.NoError(t, dec.Decode(&response))
|
||||
|
||||
opts := &ChecksOptions{
|
||||
IO: io,
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
IO: ios,
|
||||
SelectorArg: "123",
|
||||
}
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
|
||||
if tt.stubs != nil {
|
||||
tt.stubs(reg)
|
||||
} else if tt.fixture != "" {
|
||||
reg.Register(httpmock.GraphQL(`query PullRequestByNumber\b`), httpmock.FileResponse(tt.fixture))
|
||||
}
|
||||
|
||||
opts.HttpClient = func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
Finder: shared.NewMockFinder("123", response, ghrepo.New("OWNER", "REPO")),
|
||||
}
|
||||
|
||||
err := checksRun(opts)
|
||||
|
|
@ -232,10 +210,6 @@ func TestChecksRun_web(t *testing.T) {
|
|||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
browser := &cmdutil.TestBrowser{}
|
||||
reg := &httpmock.Registry{}
|
||||
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`), httpmock.FileResponse("./fixtures/allPassing.json"))
|
||||
|
||||
io, _, stdout, stderr := iostreams.Test()
|
||||
io.SetStdoutTTY(tc.isTTY)
|
||||
|
|
@ -246,21 +220,15 @@ func TestChecksRun_web(t *testing.T) {
|
|||
defer teardown(t)
|
||||
|
||||
err := checksRun(&ChecksOptions{
|
||||
IO: io,
|
||||
Browser: browser,
|
||||
WebMode: true,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
IO: io,
|
||||
Browser: browser,
|
||||
WebMode: true,
|
||||
SelectorArg: "123",
|
||||
Finder: shared.NewMockFinder("123", &api.PullRequest{Number: 123}, ghrepo.New("OWNER", "REPO")),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.wantStdout, stdout.String())
|
||||
assert.Equal(t, tc.wantStderr, stderr.String())
|
||||
reg.Verify(t)
|
||||
browser.Verify(t, tc.wantBrowse)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{ "data": { "repository": { "pullRequest": {
|
||||
{
|
||||
"number": 123,
|
||||
"commits": {
|
||||
"statusCheckRollup": {
|
||||
"nodes": [
|
||||
{
|
||||
"commit": {
|
||||
|
|
@ -37,5 +37,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
]}
|
||||
} } } }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{ "data": { "repository": { "pullRequest": {
|
||||
{
|
||||
"number": 123,
|
||||
"commits": {
|
||||
"statusCheckRollup": {
|
||||
"nodes": [
|
||||
{
|
||||
"commit": {
|
||||
|
|
@ -37,5 +37,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
]}
|
||||
} } } }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{ "data": { "repository": { "pullRequest": {
|
||||
{
|
||||
"number": 123,
|
||||
"commits": {
|
||||
"statusCheckRollup": {
|
||||
"nodes": [
|
||||
{
|
||||
"commit": {
|
||||
|
|
@ -37,5 +37,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
]}
|
||||
} } } }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{ "data": { "repository": { "pullRequest": {
|
||||
{
|
||||
"number": 123,
|
||||
"commits": {
|
||||
"statusCheckRollup": {
|
||||
"nodes": [
|
||||
{
|
||||
"commit": {
|
||||
|
|
@ -34,5 +34,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
]}
|
||||
} } } }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ import (
|
|||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"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/iostreams"
|
||||
|
|
@ -16,11 +14,11 @@ import (
|
|||
|
||||
type CloseOptions struct {
|
||||
HttpClient func() (*http.Client, error)
|
||||
Config func() (config.Config, error)
|
||||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Branch func() (string, error)
|
||||
|
||||
Finder shared.PRFinder
|
||||
|
||||
SelectorArg string
|
||||
DeleteBranch bool
|
||||
DeleteLocalBranch bool
|
||||
|
|
@ -30,7 +28,6 @@ func NewCmdClose(f *cmdutil.Factory, runF func(*CloseOptions) error) *cobra.Comm
|
|||
opts := &CloseOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
Config: f.Config,
|
||||
Branch: f.Branch,
|
||||
}
|
||||
|
||||
|
|
@ -39,8 +36,7 @@ func NewCmdClose(f *cmdutil.Factory, runF func(*CloseOptions) error) *cobra.Comm
|
|||
Short: "Close a pull request",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
opts.Finder = shared.NewFinder(f)
|
||||
|
||||
if len(args) > 0 {
|
||||
opts.SelectorArg = args[0]
|
||||
|
|
@ -62,25 +58,29 @@ func NewCmdClose(f *cmdutil.Factory, runF func(*CloseOptions) error) *cobra.Comm
|
|||
func closeRun(opts *CloseOptions) error {
|
||||
cs := opts.IO.ColorScheme()
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
findOptions := shared.FindOptions{
|
||||
Selector: opts.SelectorArg,
|
||||
Fields: []string{"state", "number", "title", "isCrossRepository", "headRefName"},
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
pr, baseRepo, err := shared.PRFromArgs(apiClient, opts.BaseRepo, nil, nil, opts.SelectorArg)
|
||||
pr, baseRepo, err := opts.Finder.Find(findOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pr.State == "MERGED" {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Pull request #%d (%s) can't be closed because it was already merged", cs.Red("!"), pr.Number, pr.Title)
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Pull request #%d (%s) can't be closed because it was already merged\n", cs.FailureIcon(), pr.Number, pr.Title)
|
||||
return cmdutil.SilentError
|
||||
} else if !pr.IsOpen() {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Pull request #%d (%s) is already closed\n", cs.Yellow("!"), pr.Number, pr.Title)
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Pull request #%d (%s) is already closed\n", cs.WarningIcon(), pr.Number, pr.Title)
|
||||
return nil
|
||||
}
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
err = api.PullRequestClose(apiClient, baseRepo, pr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("API call failed: %w", err)
|
||||
|
|
@ -88,12 +88,10 @@ func closeRun(opts *CloseOptions) error {
|
|||
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Closed pull request #%d (%s)\n", cs.SuccessIconWithColor(cs.Red), pr.Number, pr.Title)
|
||||
|
||||
crossRepoPR := pr.HeadRepositoryOwner.Login != baseRepo.RepoOwner()
|
||||
|
||||
if opts.DeleteBranch {
|
||||
branchSwitchString := ""
|
||||
|
||||
if opts.DeleteLocalBranch && !crossRepoPR {
|
||||
if opts.DeleteLocalBranch {
|
||||
currentBranch, err := opts.Branch()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -113,10 +111,8 @@ func closeRun(opts *CloseOptions) error {
|
|||
|
||||
localBranchExists := git.HasLocalBranch(pr.HeadRefName)
|
||||
if localBranchExists {
|
||||
err = git.DeleteLocalBranch(pr.HeadRefName)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to delete local branch %s: %w", cs.Cyan(pr.HeadRefName), err)
|
||||
return err
|
||||
if err := git.DeleteLocalBranch(pr.HeadRefName); err != nil {
|
||||
return fmt.Errorf("failed to delete local branch %s: %w", cs.Cyan(pr.HeadRefName), err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -125,11 +121,14 @@ func closeRun(opts *CloseOptions) error {
|
|||
}
|
||||
}
|
||||
|
||||
if !crossRepoPR {
|
||||
err = api.BranchDeleteRemote(apiClient, baseRepo, pr.HeadRefName)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to delete remote branch %s: %w", cs.Cyan(pr.HeadRefName), err)
|
||||
return err
|
||||
if pr.IsCrossRepository {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Skipped deleting the remote branch of a pull request from fork\n", cs.WarningIcon())
|
||||
if !opts.DeleteLocalBranch {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
if err := api.BranchDeleteRemote(apiClient, baseRepo, pr.HeadRefName); err != nil {
|
||||
return fmt.Errorf("failed to delete remote branch %s: %w", cs.Cyan(pr.HeadRefName), err)
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Deleted branch %s%s\n", cs.SuccessIconWithColor(cs.Red), cs.Cyan(pr.HeadRefName), branchSwitchString)
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import (
|
|||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/internal/run"
|
||||
"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"
|
||||
|
|
@ -18,6 +20,44 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// repo: either "baseOwner/baseRepo" or "baseOwner/baseRepo:defaultBranch"
|
||||
// prHead: "headOwner/headRepo:headBranch"
|
||||
func stubPR(repo, prHead string) (ghrepo.Interface, *api.PullRequest) {
|
||||
defaultBranch := ""
|
||||
if idx := strings.IndexRune(repo, ':'); idx >= 0 {
|
||||
defaultBranch = repo[idx+1:]
|
||||
repo = repo[:idx]
|
||||
}
|
||||
baseRepo, err := ghrepo.FromFullName(repo)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if defaultBranch != "" {
|
||||
baseRepo = api.InitRepoHostname(&api.Repository{
|
||||
Name: baseRepo.RepoName(),
|
||||
Owner: api.RepositoryOwner{Login: baseRepo.RepoOwner()},
|
||||
DefaultBranchRef: api.BranchRef{Name: defaultBranch},
|
||||
}, baseRepo.RepoHost())
|
||||
}
|
||||
|
||||
idx := strings.IndexRune(prHead, ':')
|
||||
headRefName := prHead[idx+1:]
|
||||
headRepo, err := ghrepo.FromFullName(prHead[:idx])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return baseRepo, &api.PullRequest{
|
||||
ID: "THE-ID",
|
||||
Number: 96,
|
||||
State: "OPEN",
|
||||
HeadRefName: headRefName,
|
||||
HeadRepositoryOwner: api.Owner{Login: headRepo.RepoOwner()},
|
||||
HeadRepository: &api.PRRepository{Name: headRepo.RepoName()},
|
||||
IsCrossRepository: !ghrepo.IsSame(baseRepo, headRepo),
|
||||
}
|
||||
}
|
||||
|
||||
func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, error) {
|
||||
io, _, stdout, stderr := iostreams.Test()
|
||||
io.SetStdoutTTY(isTTY)
|
||||
|
|
@ -29,12 +69,6 @@ func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, err
|
|||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: rt}, nil
|
||||
},
|
||||
Config: func() (config.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
Branch: func() (string, error) {
|
||||
return "trunk", nil
|
||||
},
|
||||
|
|
@ -63,13 +97,10 @@ func TestPrClose(t *testing.T) {
|
|||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"pullRequest": { "id": "THE-ID", "number": 96, "title": "The title of the PR", "state": "OPEN" }
|
||||
} } }`),
|
||||
)
|
||||
baseRepo, pr := stubPR("OWNER/REPO", "OWNER/REPO:feature")
|
||||
pr.Title = "The title of the PR"
|
||||
shared.RunCommandFinder("96", pr, baseRepo)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestClose\b`),
|
||||
httpmock.GraphQLMutation(`{"id": "THE-ID"}`,
|
||||
|
|
@ -79,57 +110,34 @@ func TestPrClose(t *testing.T) {
|
|||
)
|
||||
|
||||
output, err := runCommand(http, true, "96")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `pr close`: %v", err)
|
||||
}
|
||||
|
||||
r := regexp.MustCompile(`Closed pull request #96 \(The title of the PR\)`)
|
||||
|
||||
if !r.MatchString(output.Stderr()) {
|
||||
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "✓ Closed pull request #96 (The title of the PR)\n", output.Stderr())
|
||||
}
|
||||
|
||||
func TestPrClose_alreadyClosed(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"pullRequest": { "number": 101, "title": "The title of the PR", "state": "CLOSED" }
|
||||
} } }`),
|
||||
)
|
||||
baseRepo, pr := stubPR("OWNER/REPO", "OWNER/REPO:feature")
|
||||
pr.State = "CLOSED"
|
||||
pr.Title = "The title of the PR"
|
||||
shared.RunCommandFinder("96", pr, baseRepo)
|
||||
|
||||
output, err := runCommand(http, true, "101")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `pr close`: %v", err)
|
||||
}
|
||||
|
||||
r := regexp.MustCompile(`Pull request #101 \(The title of the PR\) is already closed`)
|
||||
|
||||
if !r.MatchString(output.Stderr()) {
|
||||
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
|
||||
}
|
||||
output, err := runCommand(http, true, "96")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "! Pull request #96 (The title of the PR) is already closed\n", output.Stderr())
|
||||
}
|
||||
|
||||
func TestPrClose_deleteBranch(t *testing.T) {
|
||||
func TestPrClose_deleteBranch_sameRepo(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"id": "THE-ID",
|
||||
"number": 96,
|
||||
"title": "The title of the PR",
|
||||
"headRefName":"blueberries",
|
||||
"headRepositoryOwner": {"login": "OWNER"},
|
||||
"state": "OPEN" }
|
||||
} } }`),
|
||||
)
|
||||
baseRepo, pr := stubPR("OWNER/REPO", "OWNER/REPO:blueberries")
|
||||
pr.Title = "The title of the PR"
|
||||
shared.RunCommandFinder("96", pr, baseRepo)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestClose\b`),
|
||||
httpmock.GraphQLMutation(`{"id": "THE-ID"}`,
|
||||
|
|
@ -148,10 +156,77 @@ func TestPrClose_deleteBranch(t *testing.T) {
|
|||
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`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, heredoc.Doc(`
|
||||
✓ Closed pull request #96 (The title of the PR)
|
||||
✓ Deleted branch blueberries
|
||||
`), output.Stderr())
|
||||
}
|
||||
|
||||
func TestPrClose_deleteBranch_crossRepo(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
baseRepo, pr := stubPR("OWNER/REPO", "hubot/REPO:blueberries")
|
||||
pr.Title = "The title of the PR"
|
||||
shared.RunCommandFinder("96", pr, baseRepo)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestClose\b`),
|
||||
httpmock.GraphQLMutation(`{"id": "THE-ID"}`,
|
||||
func(inputs map[string]interface{}) {
|
||||
assert.Equal(t, inputs["pullRequestId"], "THE-ID")
|
||||
}),
|
||||
)
|
||||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
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`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, heredoc.Doc(`
|
||||
✓ Closed pull request #96 (The title of the PR)
|
||||
! Skipped deleting the remote branch of a pull request from fork
|
||||
✓ Deleted branch blueberries
|
||||
`), output.Stderr())
|
||||
}
|
||||
|
||||
func TestPrClose_deleteBranch_sameBranch(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
baseRepo, pr := stubPR("OWNER/REPO:main", "OWNER/REPO:trunk")
|
||||
pr.Title = "The title of the PR"
|
||||
shared.RunCommandFinder("96", pr, baseRepo)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestClose\b`),
|
||||
httpmock.GraphQLMutation(`{"id": "THE-ID"}`,
|
||||
func(inputs map[string]interface{}) {
|
||||
assert.Equal(t, inputs["pullRequestId"], "THE-ID")
|
||||
}),
|
||||
)
|
||||
http.Register(
|
||||
httpmock.REST("DELETE", "repos/OWNER/REPO/git/refs/heads/trunk"),
|
||||
httpmock.StringResponse(`{}`))
|
||||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
cs.Register(`git checkout main`, 0, "")
|
||||
cs.Register(`git rev-parse --verify refs/heads/trunk`, 0, "")
|
||||
cs.Register(`git branch -D trunk`, 0, "")
|
||||
|
||||
output, err := runCommand(http, true, `96 --delete-branch`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, heredoc.Doc(`
|
||||
✓ Closed pull request #96 (The title of the PR)
|
||||
✓ Deleted branch trunk and switched to branch main
|
||||
`), output.Stderr())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,8 @@ 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"
|
||||
|
|
@ -48,7 +45,13 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*shared.CommentableOptions) err
|
|||
if len(args) > 0 {
|
||||
selector = args[0]
|
||||
}
|
||||
opts.RetrieveCommentable = retrievePR(f.HttpClient, f.BaseRepo, f.Branch, f.Remotes, selector)
|
||||
finder := shared.NewFinder(f)
|
||||
opts.RetrieveCommentable = func() (shared.Commentable, ghrepo.Interface, error) {
|
||||
return finder.Find(shared.FindOptions{
|
||||
Selector: selector,
|
||||
Fields: []string{"id", "url"},
|
||||
})
|
||||
}
|
||||
return shared.CommentablePreRun(cmd, opts)
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
|
@ -74,24 +77,3 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*shared.CommentableOptions) err
|
|||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/cmd/pr/shared"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
|
|
@ -224,7 +224,6 @@ func Test_commentRun(t *testing.T) {
|
|||
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",
|
||||
|
|
@ -238,9 +237,6 @@ func Test_commentRun(t *testing.T) {
|
|||
|
||||
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",
|
||||
},
|
||||
{
|
||||
|
|
@ -253,7 +249,6 @@ func Test_commentRun(t *testing.T) {
|
|||
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",
|
||||
|
|
@ -266,7 +261,6 @@ func Test_commentRun(t *testing.T) {
|
|||
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",
|
||||
|
|
@ -280,16 +274,20 @@ func Test_commentRun(t *testing.T) {
|
|||
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
tt.httpStubs(t, reg)
|
||||
if tt.httpStubs != nil {
|
||||
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")
|
||||
tt.input.RetrieveCommentable = func() (shared.Commentable, ghrepo.Interface, error) {
|
||||
return &api.PullRequest{
|
||||
Number: 123,
|
||||
URL: "https://github.com/OWNER/REPO/pull/123",
|
||||
}, ghrepo.New("OWNER", "REPO"), nil
|
||||
}
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := shared.CommentableRun(tt.input)
|
||||
|
|
@ -300,17 +298,6 @@ func Test_commentRun(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
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`),
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ type CreateOptions struct {
|
|||
Remotes func() (context.Remotes, error)
|
||||
Branch func() (string, error)
|
||||
Browser browser
|
||||
Finder shared.PRFinder
|
||||
|
||||
TitleProvided bool
|
||||
BodyProvided bool
|
||||
|
|
@ -117,6 +118,8 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
`),
|
||||
Args: cmdutil.NoArgsQuoteReminder,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.Finder = shared.NewFinder(f)
|
||||
|
||||
opts.TitleProvided = cmd.Flags().Changed("title")
|
||||
opts.RepoOverride, _ = cmd.Flags().GetString("repo")
|
||||
noMaintainerEdit, _ := cmd.Flags().GetBool("no-maintainer-edit")
|
||||
|
|
@ -220,9 +223,13 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
state.Body = opts.Body
|
||||
}
|
||||
|
||||
existingPR, err := api.PullRequestForBranch(
|
||||
client, ctx.BaseRepo, ctx.BaseBranch, ctx.HeadBranchLabel, []string{"OPEN"})
|
||||
var notFound *api.NotFoundError
|
||||
existingPR, _, err := opts.Finder.Find(shared.FindOptions{
|
||||
Selector: ctx.HeadBranchLabel,
|
||||
BaseBranch: ctx.BaseBranch,
|
||||
States: []string{"OPEN"},
|
||||
Fields: []string{"url"},
|
||||
})
|
||||
var notFound *shared.NotFoundError
|
||||
if err != nil && !errors.As(err, ¬Found) {
|
||||
return fmt.Errorf("error checking for existing pull request: %w", err)
|
||||
}
|
||||
|
|
@ -674,7 +681,7 @@ func handlePush(opts CreateOptions, ctx CreateContext) error {
|
|||
// one by forking the base repository
|
||||
if headRepo == nil && ctx.IsPushEnabled {
|
||||
opts.IO.StartProgressIndicator()
|
||||
headRepo, err = api.ForkRepo(client, ctx.BaseRepo)
|
||||
headRepo, err = api.ForkRepo(client, ctx.BaseRepo, "")
|
||||
opts.IO.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error forking repo: %w", err)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
|
|
@ -17,6 +16,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/cmd/pr/shared"
|
||||
prShared "github.com/cli/cli/pkg/cmd/pr/shared"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
|
|
@ -247,12 +247,7 @@ func TestPRCreate_recover(t *testing.T) {
|
|||
defer http.Verify(t)
|
||||
|
||||
http.StubRepoInfoResponse("OWNER", "REPO", "master")
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes" : [
|
||||
] } } } }
|
||||
`))
|
||||
shared.RunCommandFinder("feature", nil, nil)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`),
|
||||
httpmock.StringResponse(`
|
||||
|
|
@ -309,8 +304,9 @@ func TestPRCreate_recover(t *testing.T) {
|
|||
},
|
||||
})
|
||||
|
||||
tmpfile, err := ioutil.TempFile(os.TempDir(), "testrecover*")
|
||||
tmpfile, err := ioutil.TempFile(t.TempDir(), "testrecover*")
|
||||
assert.NoError(t, err)
|
||||
defer tmpfile.Close()
|
||||
|
||||
state := prShared.IssueMetadataState{
|
||||
Title: "recovered title",
|
||||
|
|
@ -337,12 +333,7 @@ func TestPRCreate_nontty(t *testing.T) {
|
|||
defer http.Verify(t)
|
||||
|
||||
http.StubRepoInfoResponse("OWNER", "REPO", "master")
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes" : [
|
||||
] } } } }`),
|
||||
)
|
||||
shared.RunCommandFinder("feature", nil, nil)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
||||
httpmock.GraphQLMutation(`
|
||||
|
|
@ -379,12 +370,7 @@ func TestPRCreate(t *testing.T) {
|
|||
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" : [
|
||||
] } } } }
|
||||
`))
|
||||
shared.RunCommandFinder("feature", nil, nil)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
||||
httpmock.GraphQLMutation(`
|
||||
|
|
@ -428,12 +414,7 @@ func TestPRCreate_NoMaintainerModify(t *testing.T) {
|
|||
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" : [
|
||||
] } } } }
|
||||
`))
|
||||
shared.RunCommandFinder("feature", nil, nil)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
||||
httpmock.GraphQLMutation(`
|
||||
|
|
@ -477,12 +458,7 @@ func TestPRCreate_createFork(t *testing.T) {
|
|||
http.Register(
|
||||
httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`{"data": {"viewer": {"login": "monalisa"} } }`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes" : [
|
||||
] } } } }
|
||||
`))
|
||||
shared.RunCommandFinder("feature", nil, nil)
|
||||
http.Register(
|
||||
httpmock.REST("POST", "repos/OWNER/REPO/forks"),
|
||||
httpmock.StatusStringResponse(201, `
|
||||
|
|
@ -544,12 +520,7 @@ func TestPRCreate_pushedToNonBaseRepo(t *testing.T) {
|
|||
defer http.Verify(t)
|
||||
|
||||
http.StubRepoInfoResponse("OWNER", "REPO", "master")
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes" : [
|
||||
] } } } }
|
||||
`))
|
||||
shared.RunCommandFinder("feature", nil, nil)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
||||
httpmock.GraphQLMutation(`
|
||||
|
|
@ -588,12 +559,7 @@ func TestPRCreate_pushedToDifferentBranchName(t *testing.T) {
|
|||
defer http.Verify(t)
|
||||
|
||||
http.StubRepoInfoResponse("OWNER", "REPO", "master")
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes" : [
|
||||
] } } } }
|
||||
`))
|
||||
shared.RunCommandFinder("feature", nil, nil)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
||||
httpmock.GraphQLMutation(`
|
||||
|
|
@ -634,12 +600,7 @@ func TestPRCreate_nonLegacyTemplate(t *testing.T) {
|
|||
defer http.Verify(t)
|
||||
|
||||
http.StubRepoInfoResponse("OWNER", "REPO", "master")
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes" : [
|
||||
] } } } }
|
||||
`))
|
||||
shared.RunCommandFinder("feature", nil, nil)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
||||
httpmock.GraphQLMutation(`
|
||||
|
|
@ -684,12 +645,7 @@ func TestPRCreate_metadata(t *testing.T) {
|
|||
defer http.Verify(t)
|
||||
|
||||
http.StubRepoInfoResponse("OWNER", "REPO", "master")
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [
|
||||
] } } } }
|
||||
`))
|
||||
shared.RunCommandFinder("feature", nil, nil)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`),
|
||||
httpmock.StringResponse(`
|
||||
|
|
@ -790,15 +746,7 @@ func TestPRCreate_alreadyExists(t *testing.T) {
|
|||
defer http.Verify(t)
|
||||
|
||||
http.StubRepoInfoResponse("OWNER", "REPO", "master")
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [
|
||||
{ "url": "https://github.com/OWNER/REPO/pull/123",
|
||||
"headRefName": "feature",
|
||||
"baseRefName": "master" }
|
||||
] } } } }`),
|
||||
)
|
||||
shared.RunCommandFinder("feature", &api.PullRequest{URL: "https://github.com/OWNER/REPO/pull/123"}, ghrepo.New("OWNER", "REPO"))
|
||||
|
||||
_, err := runCommand(http, nil, "feature", true, `-t title -b body -H feature`)
|
||||
assert.EqualError(t, err, "a pull request for branch \"feature\" into branch \"master\" already exists:\nhttps://github.com/OWNER/REPO/pull/123")
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ import (
|
|||
|
||||
"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/pkg/iostreams"
|
||||
|
|
@ -22,9 +20,8 @@ import (
|
|||
type DiffOptions struct {
|
||||
HttpClient func() (*http.Client, error)
|
||||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Remotes func() (context.Remotes, error)
|
||||
Branch func() (string, error)
|
||||
|
||||
Finder shared.PRFinder
|
||||
|
||||
SelectorArg string
|
||||
UseColor string
|
||||
|
|
@ -34,8 +31,6 @@ func NewCmdDiff(f *cmdutil.Factory, runF func(*DiffOptions) error) *cobra.Comman
|
|||
opts := &DiffOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
Remotes: f.Remotes,
|
||||
Branch: f.Branch,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
|
@ -49,8 +44,7 @@ func NewCmdDiff(f *cmdutil.Factory, runF func(*DiffOptions) error) *cobra.Comman
|
|||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
opts.Finder = shared.NewFinder(f)
|
||||
|
||||
if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 {
|
||||
return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")}
|
||||
|
|
@ -81,17 +75,21 @@ func NewCmdDiff(f *cmdutil.Factory, runF func(*DiffOptions) error) *cobra.Comman
|
|||
}
|
||||
|
||||
func diffRun(opts *DiffOptions) error {
|
||||
findOptions := shared.FindOptions{
|
||||
Selector: opts.SelectorArg,
|
||||
Fields: []string{"number"},
|
||||
}
|
||||
pr, baseRepo, err := opts.Finder.Find(findOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
pr, baseRepo, err := shared.PRFromArgs(apiClient, opts.BaseRepo, opts.Branch, opts.Remotes, opts.SelectorArg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
diff, err := apiClient.PullRequestDiff(baseRepo, pr.Number)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not find pull request diff: %w", err)
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import (
|
|||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"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"
|
||||
|
|
@ -119,27 +119,6 @@ func runCommand(rt http.RoundTripper, remotes context.Remotes, isTTY bool, cli s
|
|||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: rt}, nil
|
||||
},
|
||||
Config: func() (config.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
Remotes: func() (context.Remotes, error) {
|
||||
if remotes == nil {
|
||||
return context.Remotes{
|
||||
{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
Repo: ghrepo.New("OWNER", "REPO"),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return remotes, nil
|
||||
},
|
||||
Branch: func() (string, error) {
|
||||
return "feature", nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd := NewCmdDiff(factory, nil)
|
||||
|
|
@ -161,61 +140,15 @@ func runCommand(rt http.RoundTripper, remotes context.Remotes, isTTY bool, cli s
|
|||
}, err
|
||||
}
|
||||
|
||||
func TestPRDiff_no_current_pr(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"pullRequests": { "nodes": [] }
|
||||
} } }`),
|
||||
)
|
||||
|
||||
_, err := runCommand(http, nil, false, "")
|
||||
assert.EqualError(t, err, `no pull requests found for branch "feature"`)
|
||||
}
|
||||
|
||||
func TestPRDiff_argument_not_found(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"pullRequest": { "number": 123 }
|
||||
} } }`),
|
||||
)
|
||||
http.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/pulls/123"),
|
||||
httpmock.StatusStringResponse(404, ""),
|
||||
)
|
||||
|
||||
_, err := runCommand(http, nil, false, "123")
|
||||
assert.EqualError(t, err, `could not find pull request diff: pull request not found`)
|
||||
}
|
||||
|
||||
func TestPRDiff_notty(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [
|
||||
{ "url": "https://github.com/OWNER/REPO/pull/123",
|
||||
"number": 123,
|
||||
"id": "foobar123",
|
||||
"headRefName": "feature",
|
||||
"baseRefName": "master" }
|
||||
] } } } }`),
|
||||
)
|
||||
shared.RunCommandFinder("", &api.PullRequest{Number: 123}, ghrepo.New("OWNER", "REPO"))
|
||||
|
||||
http.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/pulls/123"),
|
||||
httpmock.StringResponse(testDiff),
|
||||
)
|
||||
httpmock.StringResponse(testDiff))
|
||||
|
||||
output, err := runCommand(http, nil, false, "")
|
||||
if err != nil {
|
||||
|
|
@ -230,23 +163,14 @@ func TestPRDiff_tty(t *testing.T) {
|
|||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [
|
||||
{ "url": "https://github.com/OWNER/REPO/pull/123",
|
||||
"number": 123,
|
||||
"id": "foobar123",
|
||||
"headRefName": "feature",
|
||||
"baseRefName": "master" }
|
||||
] } } } }`),
|
||||
)
|
||||
shared.RunCommandFinder("123", &api.PullRequest{Number: 123}, ghrepo.New("OWNER", "REPO"))
|
||||
|
||||
http.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/pulls/123"),
|
||||
httpmock.StringResponse(testDiff),
|
||||
)
|
||||
|
||||
output, err := runCommand(http, nil, true, "")
|
||||
output, err := runCommand(http, nil, true, "123")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import (
|
|||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
shared "github.com/cli/cli/pkg/cmd/pr/shared"
|
||||
|
|
@ -20,10 +19,8 @@ import (
|
|||
type EditOptions struct {
|
||||
HttpClient func() (*http.Client, error)
|
||||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Remotes func() (context.Remotes, error)
|
||||
Branch func() (string, error)
|
||||
|
||||
Finder shared.PRFinder
|
||||
Surveyor Surveyor
|
||||
Fetcher EditableOptionsFetcher
|
||||
EditorRetriever EditorRetriever
|
||||
|
|
@ -38,8 +35,6 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
|
|||
opts := &EditOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
Remotes: f.Remotes,
|
||||
Branch: f.Branch,
|
||||
Surveyor: surveyor{},
|
||||
Fetcher: fetcher{},
|
||||
EditorRetriever: editorRetriever{config: f.Config},
|
||||
|
|
@ -66,8 +61,7 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
|
|||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
opts.Finder = shared.NewFinder(f)
|
||||
|
||||
if len(args) > 0 {
|
||||
opts.SelectorArg = args[0]
|
||||
|
|
@ -155,13 +149,11 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
|
|||
}
|
||||
|
||||
func editRun(opts *EditOptions) error {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
findOptions := shared.FindOptions{
|
||||
Selector: opts.SelectorArg,
|
||||
Fields: []string{"id", "url", "title", "body", "baseRefName", "reviewRequests", "assignees", "labels", "projectCards", "milestone"},
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
pr, repo, err := shared.PRFromArgs(apiClient, opts.BaseRepo, opts.Branch, opts.Remotes, opts.SelectorArg)
|
||||
pr, repo, err := opts.Finder.Find(findOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -175,7 +167,9 @@ func editRun(opts *EditOptions) error {
|
|||
editable.Assignees.Default = pr.Assignees.Logins()
|
||||
editable.Labels.Default = pr.Labels.Names()
|
||||
editable.Projects.Default = pr.ProjectCards.ProjectNames()
|
||||
editable.Milestone.Default = pr.Milestone.Title
|
||||
if pr.Milestone != nil {
|
||||
editable.Milestone.Default = pr.Milestone.Title
|
||||
}
|
||||
|
||||
if opts.Interactive {
|
||||
err = opts.Surveyor.FieldsToEdit(&editable)
|
||||
|
|
@ -184,6 +178,12 @@ func editRun(opts *EditOptions) error {
|
|||
}
|
||||
}
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
opts.IO.StartProgressIndicator()
|
||||
err = opts.Fetcher.EditableOptionsFetch(apiClient, repo, &editable)
|
||||
opts.IO.StopProgressIndicator()
|
||||
|
|
|
|||
|
|
@ -309,6 +309,9 @@ func Test_editRun(t *testing.T) {
|
|||
name: "non-interactive",
|
||||
input: &EditOptions{
|
||||
SelectorArg: "123",
|
||||
Finder: shared.NewMockFinder("123", &api.PullRequest{
|
||||
URL: "https://github.com/OWNER/REPO/pull/123",
|
||||
}, ghrepo.New("OWNER", "REPO")),
|
||||
Interactive: false,
|
||||
Editable: shared.Editable{
|
||||
Title: shared.EditableString{
|
||||
|
|
@ -351,7 +354,6 @@ func Test_editRun(t *testing.T) {
|
|||
Fetcher: testFetcher{},
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockPullRequestGet(t, reg)
|
||||
mockRepoMetadata(t, reg, false)
|
||||
mockPullRequestUpdate(t, reg)
|
||||
mockPullRequestReviewersUpdate(t, reg)
|
||||
|
|
@ -362,6 +364,9 @@ func Test_editRun(t *testing.T) {
|
|||
name: "non-interactive skip reviewers",
|
||||
input: &EditOptions{
|
||||
SelectorArg: "123",
|
||||
Finder: shared.NewMockFinder("123", &api.PullRequest{
|
||||
URL: "https://github.com/OWNER/REPO/pull/123",
|
||||
}, ghrepo.New("OWNER", "REPO")),
|
||||
Interactive: false,
|
||||
Editable: shared.Editable{
|
||||
Title: shared.EditableString{
|
||||
|
|
@ -399,7 +404,6 @@ func Test_editRun(t *testing.T) {
|
|||
Fetcher: testFetcher{},
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockPullRequestGet(t, reg)
|
||||
mockRepoMetadata(t, reg, true)
|
||||
mockPullRequestUpdate(t, reg)
|
||||
},
|
||||
|
|
@ -408,14 +412,16 @@ func Test_editRun(t *testing.T) {
|
|||
{
|
||||
name: "interactive",
|
||||
input: &EditOptions{
|
||||
SelectorArg: "123",
|
||||
SelectorArg: "123",
|
||||
Finder: shared.NewMockFinder("123", &api.PullRequest{
|
||||
URL: "https://github.com/OWNER/REPO/pull/123",
|
||||
}, ghrepo.New("OWNER", "REPO")),
|
||||
Interactive: true,
|
||||
Surveyor: testSurveyor{},
|
||||
Fetcher: testFetcher{},
|
||||
EditorRetriever: testEditorRetriever{},
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockPullRequestGet(t, reg)
|
||||
mockRepoMetadata(t, reg, false)
|
||||
mockPullRequestUpdate(t, reg)
|
||||
mockPullRequestReviewersUpdate(t, reg)
|
||||
|
|
@ -425,14 +431,16 @@ func Test_editRun(t *testing.T) {
|
|||
{
|
||||
name: "interactive skip reviewers",
|
||||
input: &EditOptions{
|
||||
SelectorArg: "123",
|
||||
SelectorArg: "123",
|
||||
Finder: shared.NewMockFinder("123", &api.PullRequest{
|
||||
URL: "https://github.com/OWNER/REPO/pull/123",
|
||||
}, ghrepo.New("OWNER", "REPO")),
|
||||
Interactive: true,
|
||||
Surveyor: testSurveyor{skipReviewers: true},
|
||||
Fetcher: testFetcher{},
|
||||
EditorRetriever: testEditorRetriever{},
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockPullRequestGet(t, reg)
|
||||
mockRepoMetadata(t, reg, true)
|
||||
mockPullRequestUpdate(t, reg)
|
||||
},
|
||||
|
|
@ -450,11 +458,9 @@ func Test_editRun(t *testing.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 = httpClient
|
||||
tt.input.BaseRepo = baseRepo
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := editRun(tt.input)
|
||||
|
|
@ -465,18 +471,6 @@ func Test_editRun(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func mockPullRequestGet(_ *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"id": "456",
|
||||
"number": 123,
|
||||
"url": "https://github.com/OWNER/REPO/pull/123"
|
||||
} } } }`),
|
||||
)
|
||||
}
|
||||
|
||||
func mockRepoMetadata(_ *testing.T, reg *httpmock.Registry, skipReviewers bool) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query RepositoryAssignableUsers\b`),
|
||||
|
|
@ -551,23 +545,13 @@ func mockRepoMetadata(_ *testing.T, reg *httpmock.Registry, skipReviewers bool)
|
|||
func mockPullRequestUpdate(t *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestUpdate\b`),
|
||||
httpmock.GraphQLMutation(`
|
||||
{ "data": { "updatePullRequest": { "pullRequest": {
|
||||
"id": "456"
|
||||
} } } }`,
|
||||
func(inputs map[string]interface{}) {}),
|
||||
)
|
||||
httpmock.StringResponse(`{}`))
|
||||
}
|
||||
|
||||
func mockPullRequestReviewersUpdate(t *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestUpdateRequestReviews\b`),
|
||||
httpmock.GraphQLMutation(`
|
||||
{ "data": { "requestReviews": { "pullRequest": {
|
||||
"id": "456"
|
||||
} } } }`,
|
||||
func(inputs map[string]interface{}) {}),
|
||||
)
|
||||
httpmock.StringResponse(`{}`))
|
||||
}
|
||||
|
||||
type testFetcher struct{}
|
||||
|
|
|
|||
|
|
@ -155,8 +155,7 @@ func listRun(opts *ListOptions) error {
|
|||
defer opts.IO.StopPager()
|
||||
|
||||
if opts.Exporter != nil {
|
||||
data := api.ExportPRs(listResult.PullRequests, opts.Exporter.Fields())
|
||||
return opts.Exporter.Write(opts.IO.Out, data, opts.IO.ColorEnabled())
|
||||
return opts.Exporter.Write(opts.IO.Out, listResult.PullRequests, opts.IO.ColorEnabled())
|
||||
}
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
|
|
|
|||
|
|
@ -8,10 +8,8 @@ import (
|
|||
"github.com/AlecAivazis/survey/v2"
|
||||
"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"
|
||||
"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/iostreams"
|
||||
|
|
@ -26,12 +24,11 @@ type editor interface {
|
|||
|
||||
type MergeOptions struct {
|
||||
HttpClient func() (*http.Client, error)
|
||||
Config func() (config.Config, error)
|
||||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Remotes func() (context.Remotes, error)
|
||||
Branch func() (string, error)
|
||||
|
||||
Finder shared.PRFinder
|
||||
|
||||
SelectorArg string
|
||||
DeleteBranch bool
|
||||
MergeMethod PullRequestMergeMethod
|
||||
|
|
@ -52,8 +49,6 @@ func NewCmdMerge(f *cmdutil.Factory, runF func(*MergeOptions) error) *cobra.Comm
|
|||
opts := &MergeOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
Config: f.Config,
|
||||
Remotes: f.Remotes,
|
||||
Branch: f.Branch,
|
||||
}
|
||||
|
||||
|
|
@ -76,8 +71,7 @@ func NewCmdMerge(f *cmdutil.Factory, runF func(*MergeOptions) error) *cobra.Comm
|
|||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
opts.Finder = shared.NewFinder(f)
|
||||
|
||||
if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 {
|
||||
return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")}
|
||||
|
|
@ -136,7 +130,7 @@ func NewCmdMerge(f *cmdutil.Factory, runF func(*MergeOptions) error) *cobra.Comm
|
|||
|
||||
opts.Editor = &userEditor{
|
||||
io: opts.IO,
|
||||
config: opts.Config,
|
||||
config: f.Config,
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
|
|
@ -160,19 +154,23 @@ func NewCmdMerge(f *cmdutil.Factory, runF func(*MergeOptions) error) *cobra.Comm
|
|||
func mergeRun(opts *MergeOptions) error {
|
||||
cs := opts.IO.ColorScheme()
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
findOptions := shared.FindOptions{
|
||||
Selector: opts.SelectorArg,
|
||||
Fields: []string{"id", "number", "state", "title", "commits", "mergeable", "headRepositoryOwner", "headRefName"},
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
pr, baseRepo, err := shared.PRFromArgs(apiClient, opts.BaseRepo, opts.Branch, opts.Remotes, opts.SelectorArg)
|
||||
pr, baseRepo, err := opts.Finder.Find(findOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
isTerminal := opts.IO.IsStdoutTTY()
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
if opts.AutoMergeDisable {
|
||||
err := disableAutoMerge(httpClient, baseRepo, pr.ID)
|
||||
if err != nil {
|
||||
|
|
@ -186,7 +184,7 @@ func mergeRun(opts *MergeOptions) error {
|
|||
|
||||
if opts.SelectorArg == "" && len(pr.Commits.Nodes) > 0 {
|
||||
if localBranchLastCommit, err := git.LastCommit(); err == nil {
|
||||
if localBranchLastCommit.Sha != pr.Commits.Nodes[0].Commit.Oid {
|
||||
if localBranchLastCommit.Sha != pr.Commits.Nodes[len(pr.Commits.Nodes)-1].Commit.OID {
|
||||
fmt.Fprintf(opts.IO.ErrOut,
|
||||
"%s Pull request #%d (%s) has diverged from local branch\n", cs.Yellow("!"), pr.Number, pr.Title)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,11 +13,9 @@ import (
|
|||
|
||||
"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"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/internal/run"
|
||||
"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"
|
||||
|
|
@ -197,6 +195,20 @@ func Test_NewCmdMerge(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func baseRepo(owner, repo, branch string) ghrepo.Interface {
|
||||
return api.InitRepoHostname(&api.Repository{
|
||||
Name: repo,
|
||||
Owner: api.RepositoryOwner{Login: owner},
|
||||
DefaultBranchRef: api.BranchRef{Name: branch},
|
||||
}, "github.com")
|
||||
}
|
||||
|
||||
func stubCommit(pr *api.PullRequest, oid string) {
|
||||
pr.Commits.Nodes = append(pr.Commits.Nodes, api.PullRequestCommit{
|
||||
Commit: api.PullRequestCommitCommit{OID: oid},
|
||||
})
|
||||
}
|
||||
|
||||
func runCommand(rt http.RoundTripper, branch string, isTTY bool, cli string) (*test.CmdOut, error) {
|
||||
io, _, stdout, stderr := iostreams.Test()
|
||||
io.SetStdoutTTY(isTTY)
|
||||
|
|
@ -208,24 +220,6 @@ func runCommand(rt http.RoundTripper, branch string, isTTY bool, cli string) (*t
|
|||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: rt}, nil
|
||||
},
|
||||
Config: func() (config.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return api.InitRepoHostname(&api.Repository{
|
||||
Name: "REPO",
|
||||
Owner: api.RepositoryOwner{Login: "OWNER"},
|
||||
DefaultBranchRef: api.BranchRef{Name: "master"},
|
||||
}, "github.com"), nil
|
||||
},
|
||||
Remotes: func() (context.Remotes, error) {
|
||||
return context.Remotes{
|
||||
{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
Repo: ghrepo.New("OWNER", "REPO"),
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
Branch: func() (string, error) {
|
||||
return branch, nil
|
||||
},
|
||||
|
|
@ -259,17 +253,18 @@ func initFakeHTTP() *httpmock.Registry {
|
|||
func TestPrMerge(t *testing.T) {
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"id": "THE-ID",
|
||||
"number": 1,
|
||||
"title": "The title of the PR",
|
||||
"state": "OPEN",
|
||||
"headRefName": "blueberries",
|
||||
"headRepositoryOwner": {"login": "OWNER"}
|
||||
} } } }`))
|
||||
|
||||
shared.RunCommandFinder(
|
||||
"1",
|
||||
&api.PullRequest{
|
||||
ID: "THE-ID",
|
||||
Number: 1,
|
||||
State: "OPEN",
|
||||
Title: "The title of the PR",
|
||||
},
|
||||
baseRepo("OWNER", "REPO", "master"),
|
||||
)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestMerge\b`),
|
||||
httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) {
|
||||
|
|
@ -296,17 +291,18 @@ func TestPrMerge(t *testing.T) {
|
|||
func TestPrMerge_nontty(t *testing.T) {
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"id": "THE-ID",
|
||||
"number": 1,
|
||||
"title": "The title of the PR",
|
||||
"state": "OPEN",
|
||||
"headRefName": "blueberries",
|
||||
"headRepositoryOwner": {"login": "OWNER"}
|
||||
} } } }`))
|
||||
|
||||
shared.RunCommandFinder(
|
||||
"1",
|
||||
&api.PullRequest{
|
||||
ID: "THE-ID",
|
||||
Number: 1,
|
||||
State: "OPEN",
|
||||
Title: "The title of the PR",
|
||||
},
|
||||
baseRepo("OWNER", "REPO", "master"),
|
||||
)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestMerge\b`),
|
||||
httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) {
|
||||
|
|
@ -330,17 +326,18 @@ func TestPrMerge_nontty(t *testing.T) {
|
|||
func TestPrMerge_withRepoFlag(t *testing.T) {
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"id": "THE-ID",
|
||||
"number": 1,
|
||||
"title": "The title of the PR",
|
||||
"state": "OPEN",
|
||||
"headRefName": "blueberries",
|
||||
"headRepositoryOwner": {"login": "OWNER"}
|
||||
} } } }`))
|
||||
|
||||
shared.RunCommandFinder(
|
||||
"1",
|
||||
&api.PullRequest{
|
||||
ID: "THE-ID",
|
||||
Number: 1,
|
||||
State: "OPEN",
|
||||
Title: "The title of the PR",
|
||||
},
|
||||
baseRepo("OWNER", "REPO", "master"),
|
||||
)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestMerge\b`),
|
||||
httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) {
|
||||
|
|
@ -367,10 +364,19 @@ func TestPrMerge_withRepoFlag(t *testing.T) {
|
|||
func TestPrMerge_deleteBranch(t *testing.T) {
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
// FIXME: references fixture from another package
|
||||
httpmock.FileResponse("../view/fixtures/prViewPreviewWithMetadataByBranch.json"))
|
||||
|
||||
shared.RunCommandFinder(
|
||||
"",
|
||||
&api.PullRequest{
|
||||
ID: "PR_10",
|
||||
Number: 10,
|
||||
State: "OPEN",
|
||||
Title: "Blueberries are a good fruit",
|
||||
HeadRefName: "blueberries",
|
||||
},
|
||||
baseRepo("OWNER", "REPO", "master"),
|
||||
)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestMerge\b`),
|
||||
httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) {
|
||||
|
|
@ -385,8 +391,6 @@ func TestPrMerge_deleteBranch(t *testing.T) {
|
|||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
cs.Register(`git .+ show .+ HEAD`, 1, "")
|
||||
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, "")
|
||||
|
|
@ -406,10 +410,19 @@ func TestPrMerge_deleteBranch(t *testing.T) {
|
|||
func TestPrMerge_deleteNonCurrentBranch(t *testing.T) {
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
// FIXME: references fixture from another package
|
||||
httpmock.FileResponse("../view/fixtures/prViewPreviewWithMetadataByBranch.json"))
|
||||
|
||||
shared.RunCommandFinder(
|
||||
"blueberries",
|
||||
&api.PullRequest{
|
||||
ID: "PR_10",
|
||||
Number: 10,
|
||||
State: "OPEN",
|
||||
Title: "Blueberries are a good fruit",
|
||||
HeadRefName: "blueberries",
|
||||
},
|
||||
baseRepo("OWNER", "REPO", "master"),
|
||||
)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestMerge\b`),
|
||||
httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) {
|
||||
|
|
@ -439,59 +452,24 @@ func TestPrMerge_deleteNonCurrentBranch(t *testing.T) {
|
|||
`), output.Stderr())
|
||||
}
|
||||
|
||||
func TestPrMerge_noPrNumberGiven(t *testing.T) {
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
// FIXME: references fixture from another package
|
||||
httpmock.FileResponse("../view/fixtures/prViewPreviewWithMetadataByBranch.json"))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestMerge\b`),
|
||||
httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) {
|
||||
assert.Equal(t, "PR_10", input["pullRequestId"].(string))
|
||||
assert.Equal(t, "MERGE", input["mergeMethod"].(string))
|
||||
assert.NotContains(t, input, "commitHeadline")
|
||||
}))
|
||||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
cs.Register(`git .+ show .+ HEAD`, 1, "")
|
||||
cs.Register(`git config --get-regexp.+branch\\\.blueberries\\\.`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "pr merge --merge")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `pr merge`: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, heredoc.Doc(`
|
||||
✓ Merged pull request #10 (Blueberries are a good fruit)
|
||||
`), output.Stderr())
|
||||
}
|
||||
|
||||
func Test_nonDivergingPullRequest(t *testing.T) {
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [{
|
||||
"headRefName": "blueberries",
|
||||
"headRepositoryOwner": {"login": "OWNER"},
|
||||
"id": "PR_10",
|
||||
"title": "Blueberries are a good fruit",
|
||||
"number": 10,
|
||||
"commits": {
|
||||
"nodes": [{
|
||||
"commit": {
|
||||
"oid": "COMMITSHA1"
|
||||
}
|
||||
}],
|
||||
"totalCount": 1
|
||||
}
|
||||
}] } } } }`))
|
||||
|
||||
pr := &api.PullRequest{
|
||||
ID: "PR_10",
|
||||
Number: 10,
|
||||
Title: "Blueberries are a good fruit",
|
||||
State: "OPEN",
|
||||
}
|
||||
stubCommit(pr, "COMMITSHA1")
|
||||
|
||||
shared.RunCommandFinder(
|
||||
"",
|
||||
pr,
|
||||
baseRepo("OWNER", "REPO", "master"),
|
||||
)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestMerge\b`),
|
||||
httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) {
|
||||
|
|
@ -504,7 +482,6 @@ func Test_nonDivergingPullRequest(t *testing.T) {
|
|||
defer cmdTeardown(t)
|
||||
|
||||
cs.Register(`git .+ show .+ HEAD`, 0, "COMMITSHA1,title")
|
||||
cs.Register(`git config --get-regexp.+branch\\\.blueberries\\\.`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "pr merge --merge")
|
||||
if err != nil {
|
||||
|
|
@ -519,24 +496,21 @@ func Test_nonDivergingPullRequest(t *testing.T) {
|
|||
func Test_divergingPullRequestWarning(t *testing.T) {
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [{
|
||||
"headRefName": "blueberries",
|
||||
"headRepositoryOwner": {"login": "OWNER"},
|
||||
"id": "PR_10",
|
||||
"title": "Blueberries are a good fruit",
|
||||
"number": 10,
|
||||
"commits": {
|
||||
"nodes": [{
|
||||
"commit": {
|
||||
"oid": "COMMITSHA1"
|
||||
}
|
||||
}],
|
||||
"totalCount": 1
|
||||
}
|
||||
}] } } } }`))
|
||||
|
||||
pr := &api.PullRequest{
|
||||
ID: "PR_10",
|
||||
Number: 10,
|
||||
Title: "Blueberries are a good fruit",
|
||||
State: "OPEN",
|
||||
}
|
||||
stubCommit(pr, "COMMITSHA1")
|
||||
|
||||
shared.RunCommandFinder(
|
||||
"",
|
||||
pr,
|
||||
baseRepo("OWNER", "REPO", "master"),
|
||||
)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestMerge\b`),
|
||||
httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) {
|
||||
|
|
@ -549,7 +523,6 @@ func Test_divergingPullRequestWarning(t *testing.T) {
|
|||
defer cmdTeardown(t)
|
||||
|
||||
cs.Register(`git .+ show .+ HEAD`, 0, "COMMITSHA2,title")
|
||||
cs.Register(`git config --get-regexp.+branch\\\.blueberries\\\.`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "pr merge --merge")
|
||||
if err != nil {
|
||||
|
|
@ -565,20 +538,18 @@ func Test_divergingPullRequestWarning(t *testing.T) {
|
|||
func Test_pullRequestWithoutCommits(t *testing.T) {
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [{
|
||||
"headRefName": "blueberries",
|
||||
"headRepositoryOwner": {"login": "OWNER"},
|
||||
"id": "PR_10",
|
||||
"title": "Blueberries are a good fruit",
|
||||
"number": 10,
|
||||
"commits": {
|
||||
"nodes": [],
|
||||
"totalCount": 0
|
||||
}
|
||||
}] } } } }`))
|
||||
|
||||
shared.RunCommandFinder(
|
||||
"",
|
||||
&api.PullRequest{
|
||||
ID: "PR_10",
|
||||
Number: 10,
|
||||
Title: "Blueberries are a good fruit",
|
||||
State: "OPEN",
|
||||
},
|
||||
baseRepo("OWNER", "REPO", "master"),
|
||||
)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestMerge\b`),
|
||||
httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) {
|
||||
|
|
@ -587,11 +558,9 @@ func Test_pullRequestWithoutCommits(t *testing.T) {
|
|||
assert.NotContains(t, input, "commitHeadline")
|
||||
}))
|
||||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
_, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
cs.Register(`git config --get-regexp.+branch\\\.blueberries\\\.`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "pr merge --merge")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `pr merge`: %v", err)
|
||||
|
|
@ -605,17 +574,18 @@ func Test_pullRequestWithoutCommits(t *testing.T) {
|
|||
func TestPrMerge_rebase(t *testing.T) {
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"id": "THE-ID",
|
||||
"number": 2,
|
||||
"title": "The title of the PR",
|
||||
"state": "OPEN",
|
||||
"headRefName": "blueberries",
|
||||
"headRepositoryOwner": {"login": "OWNER"}
|
||||
} } } }`))
|
||||
|
||||
shared.RunCommandFinder(
|
||||
"2",
|
||||
&api.PullRequest{
|
||||
ID: "THE-ID",
|
||||
Number: 2,
|
||||
Title: "The title of the PR",
|
||||
State: "OPEN",
|
||||
},
|
||||
baseRepo("OWNER", "REPO", "master"),
|
||||
)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestMerge\b`),
|
||||
httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) {
|
||||
|
|
@ -642,17 +612,18 @@ func TestPrMerge_rebase(t *testing.T) {
|
|||
func TestPrMerge_squash(t *testing.T) {
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"id": "THE-ID",
|
||||
"number": 3,
|
||||
"title": "The title of the PR",
|
||||
"state": "OPEN",
|
||||
"headRefName": "blueberries",
|
||||
"headRepositoryOwner": {"login": "OWNER"}
|
||||
} } } }`))
|
||||
|
||||
shared.RunCommandFinder(
|
||||
"3",
|
||||
&api.PullRequest{
|
||||
ID: "THE-ID",
|
||||
Number: 3,
|
||||
Title: "The title of the PR",
|
||||
State: "OPEN",
|
||||
},
|
||||
baseRepo("OWNER", "REPO", "master"),
|
||||
)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestMerge\b`),
|
||||
httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) {
|
||||
|
|
@ -678,22 +649,18 @@ func TestPrMerge_squash(t *testing.T) {
|
|||
func TestPrMerge_alreadyMerged(t *testing.T) {
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"pullRequest": {
|
||||
"number": 4,
|
||||
"title": "The title of the PR",
|
||||
"state": "MERGED",
|
||||
"baseRefName": "master",
|
||||
"headRefName": "blueberries",
|
||||
"headRepositoryOwner": {
|
||||
"login": "OWNER"
|
||||
},
|
||||
"isCrossRepository": false
|
||||
}
|
||||
} } }`))
|
||||
|
||||
shared.RunCommandFinder(
|
||||
"4",
|
||||
&api.PullRequest{
|
||||
ID: "THE-ID",
|
||||
Number: 4,
|
||||
State: "MERGED",
|
||||
HeadRefName: "blueberries",
|
||||
BaseRefName: "master",
|
||||
},
|
||||
baseRepo("OWNER", "REPO", "master"),
|
||||
)
|
||||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
|
@ -707,23 +674,25 @@ func TestPrMerge_alreadyMerged(t *testing.T) {
|
|||
as.StubOne(true)
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "pr merge 4")
|
||||
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(), "✓ Deleted branch blueberries and switched to branch master")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "✓ Deleted branch blueberries and switched to branch master\n", output.Stderr())
|
||||
}
|
||||
|
||||
func TestPrMerge_alreadyMerged_nonInteractive(t *testing.T) {
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"pullRequest": { "number": 4, "title": "The title of the PR", "state": "MERGED"}
|
||||
} } }`))
|
||||
|
||||
shared.RunCommandFinder(
|
||||
"4",
|
||||
&api.PullRequest{
|
||||
ID: "THE-ID",
|
||||
Number: 4,
|
||||
State: "MERGED",
|
||||
HeadRepositoryOwner: api.Owner{Login: "monalisa"},
|
||||
},
|
||||
baseRepo("OWNER", "REPO", "master"),
|
||||
)
|
||||
|
||||
_, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
|
@ -740,15 +709,18 @@ func TestPrMerge_alreadyMerged_nonInteractive(t *testing.T) {
|
|||
func TestPRMerge_interactive(t *testing.T) {
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [{
|
||||
"headRefName": "blueberries",
|
||||
"headRepositoryOwner": {"login": "OWNER"},
|
||||
"id": "THE-ID",
|
||||
"number": 3
|
||||
}] } } } }`))
|
||||
|
||||
shared.RunCommandFinder(
|
||||
"",
|
||||
&api.PullRequest{
|
||||
ID: "THE-ID",
|
||||
Number: 3,
|
||||
Title: "It was the best of times",
|
||||
HeadRefName: "blueberries",
|
||||
},
|
||||
baseRepo("OWNER", "REPO", "master"),
|
||||
)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query RepositoryInfo\b`),
|
||||
httpmock.StringResponse(`
|
||||
|
|
@ -765,11 +737,9 @@ func TestPRMerge_interactive(t *testing.T) {
|
|||
assert.NotContains(t, input, "commitHeadline")
|
||||
}))
|
||||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
_, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
cs.Register(`git config --get-regexp.+branch\\\.blueberries\\\.`, 0, "")
|
||||
|
||||
as, surveyTeardown := prompt.InitAskStubber()
|
||||
defer surveyTeardown()
|
||||
|
||||
|
|
@ -789,16 +759,18 @@ func TestPRMerge_interactive(t *testing.T) {
|
|||
func TestPRMerge_interactiveWithDeleteBranch(t *testing.T) {
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [{
|
||||
"headRefName": "blueberries",
|
||||
"headRepositoryOwner": {"login": "OWNER"},
|
||||
"id": "THE-ID",
|
||||
"title": "It was the best of times",
|
||||
"number": 3
|
||||
}] } } } }`))
|
||||
|
||||
shared.RunCommandFinder(
|
||||
"",
|
||||
&api.PullRequest{
|
||||
ID: "THE-ID",
|
||||
Number: 3,
|
||||
Title: "It was the best of times",
|
||||
HeadRefName: "blueberries",
|
||||
},
|
||||
baseRepo("OWNER", "REPO", "master"),
|
||||
)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query RepositoryInfo\b`),
|
||||
httpmock.StringResponse(`
|
||||
|
|
@ -821,7 +793,6 @@ func TestPRMerge_interactiveWithDeleteBranch(t *testing.T) {
|
|||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
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, "")
|
||||
|
|
@ -851,16 +822,6 @@ func TestPRMerge_interactiveSquashEditCommitMsg(t *testing.T) {
|
|||
|
||||
tr := initFakeHTTP()
|
||||
defer tr.Verify(t)
|
||||
|
||||
tr.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"headRepositoryOwner": {"login": "OWNER"},
|
||||
"id": "THE-ID",
|
||||
"number": 3,
|
||||
"title": "title"
|
||||
} } } }`))
|
||||
tr.Register(
|
||||
httpmock.GraphQL(`query RepositoryInfo\b`),
|
||||
httpmock.StringResponse(`
|
||||
|
|
@ -902,25 +863,28 @@ func TestPRMerge_interactiveSquashEditCommitMsg(t *testing.T) {
|
|||
},
|
||||
SelectorArg: "https://github.com/OWNER/REPO/pull/123",
|
||||
InteractiveMode: true,
|
||||
Finder: shared.NewMockFinder(
|
||||
"https://github.com/OWNER/REPO/pull/123",
|
||||
&api.PullRequest{ID: "THE-ID", Number: 123, Title: "title"},
|
||||
ghrepo.New("OWNER", "REPO"),
|
||||
),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "", stdout.String())
|
||||
assert.Equal(t, "✓ Squashed and merged pull request #3 (title)\n", stderr.String())
|
||||
assert.Equal(t, "✓ Squashed and merged pull request #123 (title)\n", stderr.String())
|
||||
}
|
||||
|
||||
func TestPRMerge_interactiveCancelled(t *testing.T) {
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [{
|
||||
"headRefName": "blueberries",
|
||||
"headRepositoryOwner": {"login": "OWNER"},
|
||||
"id": "THE-ID",
|
||||
"number": 3
|
||||
}] } } } }`))
|
||||
|
||||
shared.RunCommandFinder(
|
||||
"",
|
||||
&api.PullRequest{ID: "THE-ID", Number: 123},
|
||||
ghrepo.New("OWNER", "REPO"),
|
||||
)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query RepositoryInfo\b`),
|
||||
httpmock.StringResponse(`
|
||||
|
|
@ -930,11 +894,9 @@ func TestPRMerge_interactiveCancelled(t *testing.T) {
|
|||
"squashMergeAllowed": true
|
||||
} } }`))
|
||||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
_, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
cs.Register(`git config --get-regexp.+branch\\\.blueberries\\\.`, 0, "")
|
||||
|
||||
as, surveyTeardown := prompt.InitAskStubber()
|
||||
defer surveyTeardown()
|
||||
|
||||
|
|
@ -971,18 +933,6 @@ func TestMergeRun_autoMerge(t *testing.T) {
|
|||
|
||||
tr := initFakeHTTP()
|
||||
defer tr.Verify(t)
|
||||
|
||||
tr.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"id": "THE-ID",
|
||||
"number": 123,
|
||||
"title": "The title of the PR",
|
||||
"state": "OPEN",
|
||||
"headRefName": "blueberries",
|
||||
"headRepositoryOwner": {"login": "OWNER"}
|
||||
} } } }`))
|
||||
tr.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestAutoMerge\b`),
|
||||
httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) {
|
||||
|
|
@ -1001,6 +951,11 @@ func TestMergeRun_autoMerge(t *testing.T) {
|
|||
SelectorArg: "https://github.com/OWNER/REPO/pull/123",
|
||||
AutoMergeEnable: true,
|
||||
MergeMethod: PullRequestMergeMethodSquash,
|
||||
Finder: shared.NewMockFinder(
|
||||
"https://github.com/OWNER/REPO/pull/123",
|
||||
&api.PullRequest{ID: "THE-ID", Number: 123},
|
||||
ghrepo.New("OWNER", "REPO"),
|
||||
),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
|
@ -1015,21 +970,11 @@ func TestMergeRun_disableAutoMerge(t *testing.T) {
|
|||
|
||||
tr := initFakeHTTP()
|
||||
defer tr.Verify(t)
|
||||
|
||||
tr.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"id": "THE-ID",
|
||||
"number": 123,
|
||||
"title": "The title of the PR",
|
||||
"state": "OPEN",
|
||||
"headRefName": "blueberries",
|
||||
"headRepositoryOwner": {"login": "OWNER"}
|
||||
} } } }`))
|
||||
tr.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestAutoMergeDisable\b`),
|
||||
httpmock.StringResponse(`{}`))
|
||||
httpmock.GraphQLQuery(`{}`, func(s string, m map[string]interface{}) {
|
||||
assert.Equal(t, map[string]interface{}{"prID": "THE-ID"}, m)
|
||||
}))
|
||||
|
||||
_, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
|
@ -1041,6 +986,11 @@ func TestMergeRun_disableAutoMerge(t *testing.T) {
|
|||
},
|
||||
SelectorArg: "https://github.com/OWNER/REPO/pull/123",
|
||||
AutoMergeDisable: true,
|
||||
Finder: shared.NewMockFinder(
|
||||
"https://github.com/OWNER/REPO/pull/123",
|
||||
&api.PullRequest{ID: "THE-ID", Number: 123},
|
||||
ghrepo.New("OWNER", "REPO"),
|
||||
),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,6 @@ import (
|
|||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"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/iostreams"
|
||||
|
|
@ -18,11 +15,9 @@ import (
|
|||
|
||||
type ReadyOptions struct {
|
||||
HttpClient func() (*http.Client, error)
|
||||
Config func() (config.Config, error)
|
||||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Remotes func() (context.Remotes, error)
|
||||
Branch func() (string, error)
|
||||
|
||||
Finder shared.PRFinder
|
||||
|
||||
SelectorArg string
|
||||
}
|
||||
|
|
@ -31,9 +26,6 @@ func NewCmdReady(f *cmdutil.Factory, runF func(*ReadyOptions) error) *cobra.Comm
|
|||
opts := &ReadyOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
Config: f.Config,
|
||||
Remotes: f.Remotes,
|
||||
Branch: f.Branch,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
|
@ -47,8 +39,7 @@ func NewCmdReady(f *cmdutil.Factory, runF func(*ReadyOptions) error) *cobra.Comm
|
|||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
opts.Finder = shared.NewFinder(f)
|
||||
|
||||
if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 {
|
||||
return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")}
|
||||
|
|
@ -71,25 +62,29 @@ func NewCmdReady(f *cmdutil.Factory, runF func(*ReadyOptions) error) *cobra.Comm
|
|||
func readyRun(opts *ReadyOptions) error {
|
||||
cs := opts.IO.ColorScheme()
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
findOptions := shared.FindOptions{
|
||||
Selector: opts.SelectorArg,
|
||||
Fields: []string{"id", "number", "state", "isDraft"},
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
pr, baseRepo, err := shared.PRFromArgs(apiClient, opts.BaseRepo, opts.Branch, opts.Remotes, opts.SelectorArg)
|
||||
pr, baseRepo, err := opts.Finder.Find(findOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !pr.IsOpen() {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Pull request #%d is closed. Only draft pull requests can be marked as \"ready for review\"", cs.Red("!"), pr.Number)
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Pull request #%d is closed. Only draft pull requests can be marked as \"ready for review\"\n", cs.FailureIcon(), pr.Number)
|
||||
return cmdutil.SilentError
|
||||
} else if !pr.IsDraft {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Pull request #%d is already \"ready for review\"\n", cs.Yellow("!"), pr.Number)
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Pull request #%d is already \"ready for review\"\n", cs.WarningIcon(), pr.Number)
|
||||
return nil
|
||||
}
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
err = api.PullRequestReady(apiClient, baseRepo, pr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("API call failed: %w", err)
|
||||
|
|
|
|||
|
|
@ -4,13 +4,11 @@ import (
|
|||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/api"
|
||||
"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"
|
||||
|
|
@ -101,23 +99,6 @@ func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, err
|
|||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: rt}, nil
|
||||
},
|
||||
Config: func() (config.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
Remotes: func() (context.Remotes, error) {
|
||||
return context.Remotes{
|
||||
{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
Repo: ghrepo.New("OWNER", "REPO"),
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
Branch: func() (string, error) {
|
||||
return "main", nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd := NewCmdReady(factory, nil)
|
||||
|
|
@ -143,13 +124,13 @@ func TestPRReady(t *testing.T) {
|
|||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"pullRequest": { "id": "THE-ID", "number": 444, "state": "OPEN", "isDraft": true}
|
||||
} } }`),
|
||||
)
|
||||
shared.RunCommandFinder("123", &api.PullRequest{
|
||||
ID: "THE-ID",
|
||||
Number: 123,
|
||||
State: "OPEN",
|
||||
IsDraft: true,
|
||||
}, ghrepo.New("OWNER", "REPO"))
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestReadyForReview\b`),
|
||||
httpmock.GraphQLMutation(`{"id": "THE-ID"}`,
|
||||
|
|
@ -158,62 +139,42 @@ func TestPRReady(t *testing.T) {
|
|||
}),
|
||||
)
|
||||
|
||||
output, err := runCommand(http, true, "444")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `pr ready`: %v", err)
|
||||
}
|
||||
|
||||
r := regexp.MustCompile(`Pull request #444 is marked as "ready for review"`)
|
||||
|
||||
if !r.MatchString(output.Stderr()) {
|
||||
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
|
||||
}
|
||||
output, err := runCommand(http, true, "123")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "✓ Pull request #123 is marked as \"ready for review\"\n", output.Stderr())
|
||||
}
|
||||
|
||||
func TestPRReady_alreadyReady(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"pullRequest": { "number": 445, "state": "OPEN", "isDraft": false}
|
||||
} } }`),
|
||||
)
|
||||
shared.RunCommandFinder("123", &api.PullRequest{
|
||||
ID: "THE-ID",
|
||||
Number: 123,
|
||||
State: "OPEN",
|
||||
IsDraft: false,
|
||||
}, ghrepo.New("OWNER", "REPO"))
|
||||
|
||||
output, err := runCommand(http, true, "445")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `pr ready`: %v", err)
|
||||
}
|
||||
|
||||
r := regexp.MustCompile(`Pull request #445 is already "ready for review"`)
|
||||
|
||||
if !r.MatchString(output.Stderr()) {
|
||||
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
|
||||
}
|
||||
output, err := runCommand(http, true, "123")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "! Pull request #123 is already \"ready for review\"\n", output.Stderr())
|
||||
}
|
||||
|
||||
func TestPRReady_closed(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"pullRequest": { "number": 446, "state": "CLOSED", "isDraft": true}
|
||||
} } }`),
|
||||
)
|
||||
shared.RunCommandFinder("123", &api.PullRequest{
|
||||
ID: "THE-ID",
|
||||
Number: 123,
|
||||
State: "CLOSED",
|
||||
IsDraft: true,
|
||||
}, ghrepo.New("OWNER", "REPO"))
|
||||
|
||||
output, err := runCommand(http, true, "446")
|
||||
if err == nil {
|
||||
t.Fatalf("expected an error running command `pr ready` on a review that is closed!: %v", err)
|
||||
}
|
||||
|
||||
r := regexp.MustCompile(`Pull request #446 is closed. Only draft pull requests can be marked as "ready for review"`)
|
||||
|
||||
if !r.MatchString(output.Stderr()) {
|
||||
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
|
||||
}
|
||||
output, err := runCommand(http, true, "123")
|
||||
assert.EqualError(t, err, "SilentError")
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "X Pull request #123 is closed. Only draft pull requests can be marked as \"ready for review\"\n", output.Stderr())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ import (
|
|||
"net/http"
|
||||
|
||||
"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"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
|
|
@ -15,9 +13,9 @@ import (
|
|||
|
||||
type ReopenOptions struct {
|
||||
HttpClient func() (*http.Client, error)
|
||||
Config func() (config.Config, error)
|
||||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
|
||||
Finder shared.PRFinder
|
||||
|
||||
SelectorArg string
|
||||
}
|
||||
|
|
@ -26,7 +24,6 @@ func NewCmdReopen(f *cmdutil.Factory, runF func(*ReopenOptions) error) *cobra.Co
|
|||
opts := &ReopenOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
Config: f.Config,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
|
@ -34,8 +31,7 @@ func NewCmdReopen(f *cmdutil.Factory, runF func(*ReopenOptions) error) *cobra.Co
|
|||
Short: "Reopen a pull request",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
opts.Finder = shared.NewFinder(f)
|
||||
|
||||
if len(args) > 0 {
|
||||
opts.SelectorArg = args[0]
|
||||
|
|
@ -54,27 +50,31 @@ func NewCmdReopen(f *cmdutil.Factory, runF func(*ReopenOptions) error) *cobra.Co
|
|||
func reopenRun(opts *ReopenOptions) error {
|
||||
cs := opts.IO.ColorScheme()
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
findOptions := shared.FindOptions{
|
||||
Selector: opts.SelectorArg,
|
||||
Fields: []string{"id", "number", "state", "title"},
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
pr, baseRepo, err := shared.PRFromArgs(apiClient, opts.BaseRepo, nil, nil, opts.SelectorArg)
|
||||
pr, baseRepo, err := opts.Finder.Find(findOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pr.State == "MERGED" {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Pull request #%d (%s) can't be reopened because it was already merged", cs.Red("!"), pr.Number, pr.Title)
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Pull request #%d (%s) can't be reopened because it was already merged\n", cs.FailureIcon(), pr.Number, pr.Title)
|
||||
return cmdutil.SilentError
|
||||
}
|
||||
|
||||
if pr.IsOpen() {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Pull request #%d (%s) is already open\n", cs.Yellow("!"), pr.Number, pr.Title)
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Pull request #%d (%s) is already open\n", cs.WarningIcon(), pr.Number, pr.Title)
|
||||
return nil
|
||||
}
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
err = api.PullRequestReopen(apiClient, baseRepo, pr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("API call failed: %w", err)
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import (
|
|||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/api"
|
||||
"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"
|
||||
|
|
@ -28,12 +28,6 @@ func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, err
|
|||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: rt}, nil
|
||||
},
|
||||
Config: func() (config.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd := NewCmdReopen(factory, nil)
|
||||
|
|
@ -59,13 +53,13 @@ func TestPRReopen(t *testing.T) {
|
|||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"pullRequest": { "id": "THE-ID", "number": 666, "title": "The title of the PR", "state": "CLOSED" }
|
||||
} } }`),
|
||||
)
|
||||
shared.RunCommandFinder("123", &api.PullRequest{
|
||||
ID: "THE-ID",
|
||||
Number: 123,
|
||||
State: "CLOSED",
|
||||
Title: "The title of the PR",
|
||||
}, ghrepo.New("OWNER", "REPO"))
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestReopen\b`),
|
||||
httpmock.GraphQLMutation(`{"id": "THE-ID"}`,
|
||||
|
|
@ -74,95 +68,42 @@ func TestPRReopen(t *testing.T) {
|
|||
}),
|
||||
)
|
||||
|
||||
output, err := runCommand(http, true, "666")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `pr reopen`: %v", err)
|
||||
}
|
||||
|
||||
r := regexp.MustCompile(`Reopened pull request #666 \(The title of the PR\)`)
|
||||
|
||||
if !r.MatchString(output.Stderr()) {
|
||||
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRReopen_BranchArg(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": {
|
||||
"nodes": [
|
||||
{ "id": "THE-ID", "number": 666, "title": "The title of the PR", "headRefName": "fix-bug", "state": "CLOSED" }
|
||||
]
|
||||
} } } }`),
|
||||
)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestReopen\b`),
|
||||
httpmock.GraphQLMutation(`{"id": "THE-ID"}`,
|
||||
func(inputs map[string]interface{}) {
|
||||
assert.Equal(t, inputs["pullRequestId"], "THE-ID")
|
||||
}),
|
||||
)
|
||||
|
||||
output, err := runCommand(http, true, "fix-bug")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `pr reopen`: %v", err)
|
||||
}
|
||||
|
||||
r := regexp.MustCompile(`Reopened pull request #666 \(The title of the PR\)`)
|
||||
|
||||
if !r.MatchString(output.Stderr()) {
|
||||
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
|
||||
}
|
||||
output, err := runCommand(http, true, "123")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "✓ Reopened pull request #123 (The title of the PR)\n", output.Stderr())
|
||||
}
|
||||
|
||||
func TestPRReopen_alreadyOpen(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"pullRequest": { "number": 666, "title": "The title of the PR", "state": "OPEN" }
|
||||
} } }`),
|
||||
)
|
||||
shared.RunCommandFinder("123", &api.PullRequest{
|
||||
ID: "THE-ID",
|
||||
Number: 123,
|
||||
State: "OPEN",
|
||||
Title: "The title of the PR",
|
||||
}, ghrepo.New("OWNER", "REPO"))
|
||||
|
||||
output, err := runCommand(http, true, "666")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `pr reopen`: %v", err)
|
||||
}
|
||||
|
||||
r := regexp.MustCompile(`Pull request #666 \(The title of the PR\) is already open`)
|
||||
|
||||
if !r.MatchString(output.Stderr()) {
|
||||
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
|
||||
}
|
||||
output, err := runCommand(http, true, "123")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "! Pull request #123 (The title of the PR) is already open\n", output.Stderr())
|
||||
}
|
||||
|
||||
func TestPRReopen_alreadyMerged(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"pullRequest": { "number": 666, "title": "The title of the PR", "state": "MERGED"}
|
||||
} } }`),
|
||||
)
|
||||
shared.RunCommandFinder("123", &api.PullRequest{
|
||||
ID: "THE-ID",
|
||||
Number: 123,
|
||||
State: "MERGED",
|
||||
Title: "The title of the PR",
|
||||
}, ghrepo.New("OWNER", "REPO"))
|
||||
|
||||
output, err := runCommand(http, true, "666")
|
||||
if err == nil {
|
||||
t.Fatalf("expected an error running command `pr reopen`: %v", err)
|
||||
}
|
||||
|
||||
r := regexp.MustCompile(`Pull request #666 \(The title of the PR\) can't be reopened because it was already merged`)
|
||||
|
||||
if !r.MatchString(output.Stderr()) {
|
||||
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.Stderr())
|
||||
}
|
||||
output, err := runCommand(http, true, "123")
|
||||
assert.EqualError(t, err, "SilentError")
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "X Pull request #123 (The title of the PR) can't be reopened because it was already merged\n", output.Stderr())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@ import (
|
|||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"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/iostreams"
|
||||
|
|
@ -24,9 +22,8 @@ type ReviewOptions struct {
|
|||
HttpClient func() (*http.Client, error)
|
||||
Config func() (config.Config, error)
|
||||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Remotes func() (context.Remotes, error)
|
||||
Branch func() (string, error)
|
||||
|
||||
Finder shared.PRFinder
|
||||
|
||||
SelectorArg string
|
||||
InteractiveMode bool
|
||||
|
|
@ -39,8 +36,6 @@ func NewCmdReview(f *cmdutil.Factory, runF func(*ReviewOptions) error) *cobra.Co
|
|||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
Config: f.Config,
|
||||
Remotes: f.Remotes,
|
||||
Branch: f.Branch,
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
@ -74,8 +69,7 @@ func NewCmdReview(f *cmdutil.Factory, runF func(*ReviewOptions) error) *cobra.Co
|
|||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
opts.Finder = shared.NewFinder(f)
|
||||
|
||||
if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 {
|
||||
return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")}
|
||||
|
|
@ -151,13 +145,11 @@ func NewCmdReview(f *cmdutil.Factory, runF func(*ReviewOptions) error) *cobra.Co
|
|||
}
|
||||
|
||||
func reviewRun(opts *ReviewOptions) error {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
findOptions := shared.FindOptions{
|
||||
Selector: opts.SelectorArg,
|
||||
Fields: []string{"id", "number"},
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
pr, baseRepo, err := shared.PRFromArgs(apiClient, opts.BaseRepo, opts.Branch, opts.Remotes, opts.SelectorArg)
|
||||
pr, baseRepo, err := opts.Finder.Find(findOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -183,6 +175,12 @@ func reviewRun(opts *ReviewOptions) error {
|
|||
}
|
||||
}
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
err = api.AddReview(apiClient, baseRepo, pr, reviewData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create review: %w", err)
|
||||
|
|
|
|||
|
|
@ -6,13 +6,14 @@ import (
|
|||
"io/ioutil"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"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"
|
||||
"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"
|
||||
|
|
@ -178,24 +179,6 @@ func runCommand(rt http.RoundTripper, remotes context.Remotes, isTTY bool, cli s
|
|||
Config: func() (config.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
Remotes: func() (context.Remotes, error) {
|
||||
if remotes == nil {
|
||||
return context.Remotes{
|
||||
{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
Repo: ghrepo.New("OWNER", "REPO"),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return remotes, nil
|
||||
},
|
||||
Branch: func() (string, error) {
|
||||
return "feature", nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd := NewCmdReview(factory, nil)
|
||||
|
|
@ -217,219 +200,67 @@ func runCommand(rt http.RoundTripper, remotes context.Remotes, isTTY bool, cli s
|
|||
}, err
|
||||
}
|
||||
|
||||
func TestPRReview_url_arg(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"id": "foobar123",
|
||||
"number": 123,
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO",
|
||||
"defaultBranchRef": {
|
||||
"name": "master"
|
||||
}
|
||||
},
|
||||
"isCrossRepository": false,
|
||||
"maintainerCanModify": false
|
||||
} } } }`),
|
||||
)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestReviewAdd\b`),
|
||||
httpmock.GraphQLMutation(`{"data": {} }`,
|
||||
func(inputs map[string]interface{}) {
|
||||
assert.Equal(t, inputs["pullRequestId"], "foobar123")
|
||||
assert.Equal(t, inputs["event"], "APPROVE")
|
||||
assert.Equal(t, inputs["body"], "")
|
||||
}),
|
||||
)
|
||||
|
||||
output, err := runCommand(http, nil, true, "--approve https://github.com/OWNER/REPO/pull/123")
|
||||
if err != nil {
|
||||
t.Fatalf("error running pr review: %s", err)
|
||||
}
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), "Approved pull request #123")
|
||||
}
|
||||
|
||||
func TestPRReview_number_arg(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"id": "foobar123",
|
||||
"number": 123,
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO",
|
||||
"defaultBranchRef": {
|
||||
"name": "master"
|
||||
}
|
||||
},
|
||||
"isCrossRepository": false,
|
||||
"maintainerCanModify": false
|
||||
} } } } `),
|
||||
)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestReviewAdd`),
|
||||
httpmock.GraphQLMutation(`{"data": {} }`,
|
||||
func(inputs map[string]interface{}) {
|
||||
assert.Equal(t, inputs["pullRequestId"], "foobar123")
|
||||
assert.Equal(t, inputs["event"], "APPROVE")
|
||||
assert.Equal(t, inputs["body"], "")
|
||||
}),
|
||||
)
|
||||
|
||||
output, err := runCommand(http, nil, true, "--approve 123")
|
||||
if err != nil {
|
||||
t.Fatalf("error running pr review: %s", err)
|
||||
}
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), "Approved pull request #123")
|
||||
}
|
||||
|
||||
func TestPRReview_no_arg(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [
|
||||
{ "url": "https://github.com/OWNER/REPO/pull/123",
|
||||
"number": 123,
|
||||
"id": "foobar123",
|
||||
"headRefName": "feature",
|
||||
"baseRefName": "master" }
|
||||
] } } } }`),
|
||||
)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestReviewAdd\b`),
|
||||
httpmock.GraphQLMutation(`{"data": {} }`,
|
||||
func(inputs map[string]interface{}) {
|
||||
assert.Equal(t, inputs["pullRequestId"], "foobar123")
|
||||
assert.Equal(t, inputs["event"], "COMMENT")
|
||||
assert.Equal(t, inputs["body"], "cool story")
|
||||
}),
|
||||
)
|
||||
|
||||
output, err := runCommand(http, nil, true, `--comment -b "cool story"`)
|
||||
if err != nil {
|
||||
t.Fatalf("error running pr review: %s", err)
|
||||
}
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), "Reviewed pull request #123")
|
||||
}
|
||||
|
||||
func TestPRReview(t *testing.T) {
|
||||
type c struct {
|
||||
Cmd string
|
||||
ExpectedEvent string
|
||||
ExpectedBody string
|
||||
}
|
||||
cases := []c{
|
||||
{`--request-changes -b"bad"`, "REQUEST_CHANGES", "bad"},
|
||||
{`--approve`, "APPROVE", ""},
|
||||
{`--approve -b"hot damn"`, "APPROVE", "hot damn"},
|
||||
{`--comment --body "i dunno"`, "COMMENT", "i dunno"},
|
||||
tests := []struct {
|
||||
args string
|
||||
wantEvent string
|
||||
wantBody string
|
||||
}{
|
||||
{
|
||||
args: `--request-changes -b"bad"`,
|
||||
wantEvent: "REQUEST_CHANGES",
|
||||
wantBody: "bad",
|
||||
},
|
||||
{
|
||||
args: `--approve`,
|
||||
wantEvent: "APPROVE",
|
||||
wantBody: "",
|
||||
},
|
||||
{
|
||||
args: `--approve -b"hot damn"`,
|
||||
wantEvent: "APPROVE",
|
||||
wantBody: "hot damn",
|
||||
},
|
||||
{
|
||||
args: `--comment --body "i dunno"`,
|
||||
wantEvent: "COMMENT",
|
||||
wantBody: "i dunno",
|
||||
},
|
||||
}
|
||||
|
||||
for _, kase := range cases {
|
||||
t.Run(kase.Cmd, func(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.args, func(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [
|
||||
{ "url": "https://github.com/OWNER/REPO/pull/123",
|
||||
"id": "foobar123",
|
||||
"headRefName": "feature",
|
||||
"baseRefName": "master" }
|
||||
] } } } }`),
|
||||
)
|
||||
shared.RunCommandFinder("", &api.PullRequest{ID: "THE-ID"}, ghrepo.New("OWNER", "REPO"))
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestReviewAdd\b`),
|
||||
httpmock.GraphQLMutation(`{"data": {} }`,
|
||||
func(inputs map[string]interface{}) {
|
||||
assert.Equal(t, inputs["event"], kase.ExpectedEvent)
|
||||
assert.Equal(t, inputs["body"], kase.ExpectedBody)
|
||||
assert.Equal(t, map[string]interface{}{
|
||||
"pullRequestId": "THE-ID",
|
||||
"event": tt.wantEvent,
|
||||
"body": tt.wantBody,
|
||||
}, inputs)
|
||||
}),
|
||||
)
|
||||
|
||||
_, err := runCommand(http, nil, false, kase.Cmd)
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error running %s: %s", kase.Cmd, err)
|
||||
}
|
||||
output, err := runCommand(http, nil, false, tt.args)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRReview_nontty(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [
|
||||
{ "url": "https://github.com/OWNER/REPO/pull/123",
|
||||
"number": 123,
|
||||
"id": "foobar123",
|
||||
"headRefName": "feature",
|
||||
"baseRefName": "master" }
|
||||
] } } } }`),
|
||||
)
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestReviewAdd\b`),
|
||||
httpmock.GraphQLMutation(`{"data": {} }`,
|
||||
func(inputs map[string]interface{}) {
|
||||
assert.Equal(t, inputs["event"], "COMMENT")
|
||||
assert.Equal(t, inputs["body"], "cool")
|
||||
}),
|
||||
)
|
||||
|
||||
output, err := runCommand(http, nil, false, "-c -bcool")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error running command: %s", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
}
|
||||
|
||||
func TestPRReview_interactive(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [
|
||||
{ "url": "https://github.com/OWNER/REPO/pull/123",
|
||||
"number": 123,
|
||||
"id": "foobar123",
|
||||
"headRefName": "feature",
|
||||
"baseRefName": "master" }
|
||||
] } } } }`),
|
||||
)
|
||||
shared.RunCommandFinder("", &api.PullRequest{ID: "THE-ID", Number: 123}, ghrepo.New("OWNER", "REPO"))
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestReviewAdd\b`),
|
||||
httpmock.GraphQLMutation(`{"data": {} }`,
|
||||
|
|
@ -462,33 +293,21 @@ func TestPRReview_interactive(t *testing.T) {
|
|||
})
|
||||
|
||||
output, err := runCommand(http, nil, true, "")
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error running pr review: %s", err)
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, heredoc.Doc(`
|
||||
Got:
|
||||
|
||||
//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")
|
||||
cool story
|
||||
|
||||
`), output.String())
|
||||
assert.Equal(t, "✓ Approved pull request #123\n", output.Stderr())
|
||||
}
|
||||
|
||||
func TestPRReview_interactive_no_body(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [
|
||||
{ "url": "https://github.com/OWNER/REPO/pull/123",
|
||||
"id": "foobar123",
|
||||
"headRefName": "feature",
|
||||
"baseRefName": "master" }
|
||||
] } } } }`),
|
||||
)
|
||||
shared.RunCommandFinder("", &api.PullRequest{ID: "THE-ID", Number: 123}, ghrepo.New("OWNER", "REPO"))
|
||||
|
||||
as, teardown := prompt.InitAskStubber()
|
||||
defer teardown()
|
||||
|
|
@ -520,17 +339,8 @@ func TestPRReview_interactive_blank_approve(t *testing.T) {
|
|||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [
|
||||
{ "url": "https://github.com/OWNER/REPO/pull/123",
|
||||
"number": 123,
|
||||
"id": "foobar123",
|
||||
"headRefName": "feature",
|
||||
"baseRefName": "master" }
|
||||
] } } } }`),
|
||||
)
|
||||
shared.RunCommandFinder("", &api.PullRequest{ID: "THE-ID", Number: 123}, ghrepo.New("OWNER", "REPO"))
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestReviewAdd\b`),
|
||||
httpmock.GraphQLMutation(`{"data": {} }`,
|
||||
|
|
@ -563,15 +373,7 @@ func TestPRReview_interactive_blank_approve(t *testing.T) {
|
|||
})
|
||||
|
||||
output, err := runCommand(http, nil, true, "")
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error running pr review: %s", err)
|
||||
}
|
||||
|
||||
unexpect := regexp.MustCompile("Got:")
|
||||
if unexpect.MatchString(output.String()) {
|
||||
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")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "✓ Approved pull request #123\n", output.Stderr())
|
||||
}
|
||||
|
|
|
|||
532
pkg/cmd/pr/shared/finder.go
Normal file
532
pkg/cmd/pr/shared/finder.go
Normal file
|
|
@ -0,0 +1,532 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
remotes "github.com/cli/cli/context"
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/ghinstance"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/set"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/shurcooL/graphql"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type PRFinder interface {
|
||||
Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, error)
|
||||
}
|
||||
|
||||
type progressIndicator interface {
|
||||
StartProgressIndicator()
|
||||
StopProgressIndicator()
|
||||
}
|
||||
|
||||
type finder struct {
|
||||
baseRepoFn func() (ghrepo.Interface, error)
|
||||
branchFn func() (string, error)
|
||||
remotesFn func() (remotes.Remotes, error)
|
||||
httpClient func() (*http.Client, error)
|
||||
branchConfig func(string) git.BranchConfig
|
||||
progress progressIndicator
|
||||
|
||||
repo ghrepo.Interface
|
||||
prNumber int
|
||||
branchName string
|
||||
}
|
||||
|
||||
func NewFinder(factory *cmdutil.Factory) PRFinder {
|
||||
if runCommandFinder != nil {
|
||||
f := runCommandFinder
|
||||
runCommandFinder = &mockFinder{err: errors.New("you must use a RunCommandFinder to stub PR lookups")}
|
||||
return f
|
||||
}
|
||||
|
||||
return &finder{
|
||||
baseRepoFn: factory.BaseRepo,
|
||||
branchFn: factory.Branch,
|
||||
remotesFn: factory.Remotes,
|
||||
httpClient: factory.HttpClient,
|
||||
progress: factory.IOStreams,
|
||||
branchConfig: git.ReadBranchConfig,
|
||||
}
|
||||
}
|
||||
|
||||
var runCommandFinder PRFinder
|
||||
|
||||
// RunCommandFinder is the NewMockFinder substitute to be used ONLY in runCommand-style tests.
|
||||
func RunCommandFinder(selector string, pr *api.PullRequest, repo ghrepo.Interface) *mockFinder {
|
||||
finder := NewMockFinder(selector, pr, repo)
|
||||
runCommandFinder = finder
|
||||
return finder
|
||||
}
|
||||
|
||||
type FindOptions struct {
|
||||
// Selector can be a number with optional `#` prefix, a branch name with optional `<owner>:` prefix, or
|
||||
// a PR URL.
|
||||
Selector string
|
||||
// Fields lists the GraphQL fields to fetch for the PullRequest.
|
||||
Fields []string
|
||||
// BaseBranch is the name of the base branch to scope the PR-for-branch lookup to.
|
||||
BaseBranch string
|
||||
// States lists the possible PR states to scope the PR-for-branch lookup to.
|
||||
States []string
|
||||
}
|
||||
|
||||
func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, error) {
|
||||
if len(opts.Fields) == 0 {
|
||||
return nil, nil, errors.New("Find error: no fields specified")
|
||||
}
|
||||
|
||||
if repo, prNumber, err := f.parseURL(opts.Selector); err == nil {
|
||||
f.prNumber = prNumber
|
||||
f.repo = repo
|
||||
}
|
||||
|
||||
if f.repo == nil {
|
||||
repo, err := f.baseRepoFn()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not determine base repo: %w", err)
|
||||
}
|
||||
f.repo = repo
|
||||
}
|
||||
|
||||
if opts.Selector == "" {
|
||||
if branch, prNumber, err := f.parseCurrentBranch(); err != nil {
|
||||
return nil, nil, err
|
||||
} else if prNumber > 0 {
|
||||
f.prNumber = prNumber
|
||||
} else {
|
||||
f.branchName = branch
|
||||
}
|
||||
} else if f.prNumber == 0 {
|
||||
if prNumber, err := strconv.Atoi(strings.TrimPrefix(opts.Selector, "#")); err == nil {
|
||||
f.prNumber = prNumber
|
||||
} else {
|
||||
f.branchName = opts.Selector
|
||||
}
|
||||
}
|
||||
|
||||
httpClient, err := f.httpClient()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if f.progress != nil {
|
||||
f.progress.StartProgressIndicator()
|
||||
defer f.progress.StopProgressIndicator()
|
||||
}
|
||||
|
||||
fields := set.NewStringSet()
|
||||
fields.AddValues(opts.Fields)
|
||||
numberFieldOnly := fields.Len() == 1 && fields.Contains("number")
|
||||
fields.Add("id") // for additional preload queries below
|
||||
|
||||
var pr *api.PullRequest
|
||||
if f.prNumber > 0 {
|
||||
if numberFieldOnly {
|
||||
// avoid hitting the API if we already have all the information
|
||||
return &api.PullRequest{Number: f.prNumber}, f.repo, nil
|
||||
}
|
||||
pr, err = findByNumber(httpClient, f.repo, f.prNumber, fields.ToSlice())
|
||||
} else {
|
||||
pr, err = findForBranch(httpClient, f.repo, opts.BaseBranch, f.branchName, opts.States, fields.ToSlice())
|
||||
}
|
||||
if err != nil {
|
||||
return pr, f.repo, err
|
||||
}
|
||||
|
||||
g, _ := errgroup.WithContext(context.Background())
|
||||
if fields.Contains("reviews") {
|
||||
g.Go(func() error {
|
||||
return preloadPrReviews(httpClient, f.repo, pr)
|
||||
})
|
||||
}
|
||||
if fields.Contains("comments") {
|
||||
g.Go(func() error {
|
||||
return preloadPrComments(httpClient, f.repo, pr)
|
||||
})
|
||||
}
|
||||
if fields.Contains("statusCheckRollup") {
|
||||
g.Go(func() error {
|
||||
return preloadPrChecks(httpClient, f.repo, pr)
|
||||
})
|
||||
}
|
||||
|
||||
return pr, f.repo, g.Wait()
|
||||
}
|
||||
|
||||
var pullURLRE = regexp.MustCompile(`^/([^/]+)/([^/]+)/pull/(\d+)`)
|
||||
|
||||
func (f *finder) parseURL(prURL string) (ghrepo.Interface, int, error) {
|
||||
if prURL == "" {
|
||||
return nil, 0, fmt.Errorf("invalid URL: %q", prURL)
|
||||
}
|
||||
|
||||
u, err := url.Parse(prURL)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if u.Scheme != "https" && u.Scheme != "http" {
|
||||
return nil, 0, fmt.Errorf("invalid scheme: %s", u.Scheme)
|
||||
}
|
||||
|
||||
m := pullURLRE.FindStringSubmatch(u.Path)
|
||||
if m == nil {
|
||||
return nil, 0, fmt.Errorf("not a pull request URL: %s", prURL)
|
||||
}
|
||||
|
||||
repo := ghrepo.NewWithHost(m[1], m[2], u.Hostname())
|
||||
prNumber, _ := strconv.Atoi(m[3])
|
||||
return repo, prNumber, nil
|
||||
}
|
||||
|
||||
var prHeadRE = regexp.MustCompile(`^refs/pull/(\d+)/head$`)
|
||||
|
||||
func (f *finder) parseCurrentBranch() (string, int, error) {
|
||||
prHeadRef, err := f.branchFn()
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
branchConfig := f.branchConfig(prHeadRef)
|
||||
|
||||
// the branch is configured to merge a special PR head ref
|
||||
if m := prHeadRE.FindStringSubmatch(branchConfig.MergeRef); m != nil {
|
||||
prNumber, _ := strconv.Atoi(m[1])
|
||||
return "", prNumber, nil
|
||||
}
|
||||
|
||||
var branchOwner string
|
||||
if branchConfig.RemoteURL != nil {
|
||||
// the branch merges from a remote specified by URL
|
||||
if r, err := ghrepo.FromURL(branchConfig.RemoteURL); err == nil {
|
||||
branchOwner = r.RepoOwner()
|
||||
}
|
||||
} else if branchConfig.RemoteName != "" {
|
||||
// the branch merges from a remote specified by name
|
||||
rem, _ := f.remotesFn()
|
||||
if r, err := rem.FindByName(branchConfig.RemoteName); err == nil {
|
||||
branchOwner = r.RepoOwner()
|
||||
}
|
||||
}
|
||||
|
||||
if branchOwner != "" {
|
||||
if strings.HasPrefix(branchConfig.MergeRef, "refs/heads/") {
|
||||
prHeadRef = strings.TrimPrefix(branchConfig.MergeRef, "refs/heads/")
|
||||
}
|
||||
// prepend `OWNER:` if this branch is pushed to a fork
|
||||
if !strings.EqualFold(branchOwner, f.repo.RepoOwner()) {
|
||||
prHeadRef = fmt.Sprintf("%s:%s", branchOwner, prHeadRef)
|
||||
}
|
||||
}
|
||||
|
||||
return prHeadRef, 0, nil
|
||||
}
|
||||
|
||||
func findByNumber(httpClient *http.Client, repo ghrepo.Interface, number int, fields []string) (*api.PullRequest, error) {
|
||||
type response struct {
|
||||
Repository struct {
|
||||
PullRequest api.PullRequest
|
||||
}
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
query PullRequestByNumber($owner: String!, $repo: String!, $pr_number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $pr_number) {%s}
|
||||
}
|
||||
}`, api.PullRequestGraphQL(fields))
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"owner": repo.RepoOwner(),
|
||||
"repo": repo.RepoName(),
|
||||
"pr_number": number,
|
||||
}
|
||||
|
||||
var resp response
|
||||
client := api.NewClientFromHTTP(httpClient)
|
||||
err := client.GraphQL(repo.RepoHost(), query, variables, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &resp.Repository.PullRequest, nil
|
||||
}
|
||||
|
||||
func findForBranch(httpClient *http.Client, repo ghrepo.Interface, baseBranch, headBranch string, stateFilters, fields []string) (*api.PullRequest, error) {
|
||||
type response struct {
|
||||
Repository struct {
|
||||
PullRequests struct {
|
||||
Nodes []api.PullRequest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fieldSet := set.NewStringSet()
|
||||
fieldSet.AddValues(fields)
|
||||
// these fields are required for filtering below
|
||||
fieldSet.AddValues([]string{"state", "baseRefName", "headRefName", "isCrossRepository", "headRepositoryOwner"})
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
query PullRequestForBranch($owner: String!, $repo: String!, $headRefName: String!, $states: [PullRequestState!]) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequests(headRefName: $headRefName, states: $states, first: 30, orderBy: { field: CREATED_AT, direction: DESC }) {
|
||||
nodes {%s}
|
||||
}
|
||||
}
|
||||
}`, api.PullRequestGraphQL(fieldSet.ToSlice()))
|
||||
|
||||
branchWithoutOwner := headBranch
|
||||
if idx := strings.Index(headBranch, ":"); idx >= 0 {
|
||||
branchWithoutOwner = headBranch[idx+1:]
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"owner": repo.RepoOwner(),
|
||||
"repo": repo.RepoName(),
|
||||
"headRefName": branchWithoutOwner,
|
||||
"states": stateFilters,
|
||||
}
|
||||
|
||||
var resp response
|
||||
client := api.NewClientFromHTTP(httpClient)
|
||||
err := client.GraphQL(repo.RepoHost(), query, variables, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prs := resp.Repository.PullRequests.Nodes
|
||||
sort.SliceStable(prs, func(a, b int) bool {
|
||||
return prs[a].State == "OPEN" && prs[b].State != "OPEN"
|
||||
})
|
||||
|
||||
for _, pr := range prs {
|
||||
if pr.HeadLabel() == headBranch && (baseBranch == "" || pr.BaseRefName == baseBranch) {
|
||||
return &pr, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, &NotFoundError{fmt.Errorf("no pull requests found for branch %q", headBranch)}
|
||||
}
|
||||
|
||||
func preloadPrReviews(httpClient *http.Client, repo ghrepo.Interface, pr *api.PullRequest) error {
|
||||
if !pr.Reviews.PageInfo.HasNextPage {
|
||||
return nil
|
||||
}
|
||||
|
||||
type response struct {
|
||||
Node struct {
|
||||
PullRequest struct {
|
||||
Reviews api.PullRequestReviews `graphql:"reviews(first: 100, after: $endCursor)"`
|
||||
} `graphql:"...on PullRequest"`
|
||||
} `graphql:"node(id: $id)"`
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"id": githubv4.ID(pr.ID),
|
||||
"endCursor": githubv4.String(pr.Reviews.PageInfo.EndCursor),
|
||||
}
|
||||
|
||||
gql := graphql.NewClient(ghinstance.GraphQLEndpoint(repo.RepoHost()), httpClient)
|
||||
|
||||
for {
|
||||
var query response
|
||||
err := gql.QueryNamed(context.Background(), "ReviewsForPullRequest", &query, variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pr.Reviews.Nodes = append(pr.Reviews.Nodes, query.Node.PullRequest.Reviews.Nodes...)
|
||||
pr.Reviews.TotalCount = len(pr.Reviews.Nodes)
|
||||
|
||||
if !query.Node.PullRequest.Reviews.PageInfo.HasNextPage {
|
||||
break
|
||||
}
|
||||
variables["endCursor"] = githubv4.String(query.Node.PullRequest.Reviews.PageInfo.EndCursor)
|
||||
}
|
||||
|
||||
pr.Reviews.PageInfo.HasNextPage = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func preloadPrComments(client *http.Client, repo ghrepo.Interface, pr *api.PullRequest) error {
|
||||
if !pr.Comments.PageInfo.HasNextPage {
|
||||
return nil
|
||||
}
|
||||
|
||||
type response struct {
|
||||
Node struct {
|
||||
PullRequest struct {
|
||||
Comments api.Comments `graphql:"comments(first: 100, after: $endCursor)"`
|
||||
} `graphql:"...on PullRequest"`
|
||||
} `graphql:"node(id: $id)"`
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"id": githubv4.ID(pr.ID),
|
||||
"endCursor": githubv4.String(pr.Comments.PageInfo.EndCursor),
|
||||
}
|
||||
|
||||
gql := graphql.NewClient(ghinstance.GraphQLEndpoint(repo.RepoHost()), client)
|
||||
|
||||
for {
|
||||
var query response
|
||||
err := gql.QueryNamed(context.Background(), "CommentsForPullRequest", &query, variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pr.Comments.Nodes = append(pr.Comments.Nodes, query.Node.PullRequest.Comments.Nodes...)
|
||||
pr.Comments.TotalCount = len(pr.Comments.Nodes)
|
||||
|
||||
if !query.Node.PullRequest.Comments.PageInfo.HasNextPage {
|
||||
break
|
||||
}
|
||||
variables["endCursor"] = githubv4.String(query.Node.PullRequest.Comments.PageInfo.EndCursor)
|
||||
}
|
||||
|
||||
pr.Comments.PageInfo.HasNextPage = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func preloadPrChecks(client *http.Client, repo ghrepo.Interface, pr *api.PullRequest) error {
|
||||
if len(pr.StatusCheckRollup.Nodes) == 0 {
|
||||
return nil
|
||||
}
|
||||
statusCheckRollup := &pr.StatusCheckRollup.Nodes[0].Commit.StatusCheckRollup.Contexts
|
||||
if !statusCheckRollup.PageInfo.HasNextPage {
|
||||
return nil
|
||||
}
|
||||
|
||||
endCursor := statusCheckRollup.PageInfo.EndCursor
|
||||
|
||||
type response struct {
|
||||
Node *api.PullRequest
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
query PullRequestStatusChecks($id: ID!, $endCursor: String!) {
|
||||
node(id: $id) {
|
||||
...on PullRequest {
|
||||
%s
|
||||
}
|
||||
}
|
||||
}`, api.StatusCheckRollupGraphQL("$endCursor"))
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"id": pr.ID,
|
||||
}
|
||||
|
||||
apiClient := api.NewClientFromHTTP(client)
|
||||
for {
|
||||
variables["endCursor"] = endCursor
|
||||
var resp response
|
||||
err := apiClient.GraphQL(repo.RepoHost(), query, variables, &resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result := resp.Node.StatusCheckRollup.Nodes[0].Commit.StatusCheckRollup.Contexts
|
||||
statusCheckRollup.Nodes = append(
|
||||
statusCheckRollup.Nodes,
|
||||
result.Nodes...,
|
||||
)
|
||||
|
||||
if !result.PageInfo.HasNextPage {
|
||||
break
|
||||
}
|
||||
endCursor = result.PageInfo.EndCursor
|
||||
}
|
||||
|
||||
statusCheckRollup.PageInfo.HasNextPage = false
|
||||
return nil
|
||||
}
|
||||
|
||||
type NotFoundError struct {
|
||||
error
|
||||
}
|
||||
|
||||
func (err *NotFoundError) Unwrap() error {
|
||||
return err.error
|
||||
}
|
||||
|
||||
func NewMockFinder(selector string, pr *api.PullRequest, repo ghrepo.Interface) *mockFinder {
|
||||
var err error
|
||||
if pr == nil {
|
||||
err = &NotFoundError{errors.New("no pull requests found")}
|
||||
}
|
||||
return &mockFinder{
|
||||
expectSelector: selector,
|
||||
pr: pr,
|
||||
repo: repo,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
type mockFinder struct {
|
||||
called bool
|
||||
expectSelector string
|
||||
expectFields []string
|
||||
pr *api.PullRequest
|
||||
repo ghrepo.Interface
|
||||
err error
|
||||
}
|
||||
|
||||
func (m *mockFinder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, error) {
|
||||
if m.err != nil {
|
||||
return nil, nil, m.err
|
||||
}
|
||||
if m.expectSelector != opts.Selector {
|
||||
return nil, nil, fmt.Errorf("mockFinder: expected selector %q, got %q", m.expectSelector, opts.Selector)
|
||||
}
|
||||
if len(m.expectFields) > 0 && !isEqualSet(m.expectFields, opts.Fields) {
|
||||
return nil, nil, fmt.Errorf("mockFinder: expected fields %v, got %v", m.expectFields, opts.Fields)
|
||||
}
|
||||
if m.called {
|
||||
return nil, nil, errors.New("mockFinder used more than once")
|
||||
}
|
||||
m.called = true
|
||||
|
||||
if m.pr.HeadRepositoryOwner.Login == "" {
|
||||
// pose as same-repo PR by default
|
||||
m.pr.HeadRepositoryOwner.Login = m.repo.RepoOwner()
|
||||
}
|
||||
|
||||
return m.pr, m.repo, nil
|
||||
}
|
||||
|
||||
func (m *mockFinder) ExpectFields(fields []string) {
|
||||
m.expectFields = fields
|
||||
}
|
||||
|
||||
func isEqualSet(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
aCopy := make([]string, len(a))
|
||||
copy(aCopy, a)
|
||||
bCopy := make([]string, len(b))
|
||||
copy(bCopy, b)
|
||||
sort.Strings(aCopy)
|
||||
sort.Strings(bCopy)
|
||||
|
||||
for i := range aCopy {
|
||||
if aCopy[i] != bCopy[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
394
pkg/cmd/pr/shared/finder_test.go
Normal file
394
pkg/cmd/pr/shared/finder_test.go
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
)
|
||||
|
||||
func TestFind(t *testing.T) {
|
||||
type args struct {
|
||||
baseRepoFn func() (ghrepo.Interface, error)
|
||||
branchFn func() (string, error)
|
||||
branchConfig func(string) git.BranchConfig
|
||||
remotesFn func() (context.Remotes, error)
|
||||
selector string
|
||||
fields []string
|
||||
baseBranch string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
httpStub func(*httpmock.Registry)
|
||||
wantPR int
|
||||
wantRepo string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "number argument",
|
||||
args: args{
|
||||
selector: "13",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("OWNER/REPO")
|
||||
},
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{
|
||||
"pullRequest":{"number":13}
|
||||
}}}`))
|
||||
},
|
||||
wantPR: 13,
|
||||
wantRepo: "https://github.com/OWNER/REPO",
|
||||
},
|
||||
{
|
||||
name: "baseRepo is error",
|
||||
args: args{
|
||||
selector: "13",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: func() (ghrepo.Interface, error) {
|
||||
return nil, errors.New("baseRepoErr")
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "blank fields is error",
|
||||
args: args{
|
||||
selector: "13",
|
||||
fields: []string{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "number only",
|
||||
args: args{
|
||||
selector: "13",
|
||||
fields: []string{"number"},
|
||||
baseRepoFn: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("OWNER/REPO")
|
||||
},
|
||||
},
|
||||
httpStub: nil,
|
||||
wantPR: 13,
|
||||
wantRepo: "https://github.com/OWNER/REPO",
|
||||
},
|
||||
{
|
||||
name: "number with hash argument",
|
||||
args: args{
|
||||
selector: "#13",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("OWNER/REPO")
|
||||
},
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{
|
||||
"pullRequest":{"number":13}
|
||||
}}}`))
|
||||
},
|
||||
wantPR: 13,
|
||||
wantRepo: "https://github.com/OWNER/REPO",
|
||||
},
|
||||
{
|
||||
name: "URL argument",
|
||||
args: args{
|
||||
selector: "https://example.org/OWNER/REPO/pull/13/files",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: nil,
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{
|
||||
"pullRequest":{"number":13}
|
||||
}}}`))
|
||||
},
|
||||
wantPR: 13,
|
||||
wantRepo: "https://example.org/OWNER/REPO",
|
||||
},
|
||||
{
|
||||
name: "branch argument",
|
||||
args: args{
|
||||
selector: "blueberries",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("OWNER/REPO")
|
||||
},
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{
|
||||
"pullRequests":{"nodes":[
|
||||
{
|
||||
"number": 14,
|
||||
"state": "CLOSED",
|
||||
"baseRefName": "main",
|
||||
"headRefName": "blueberries",
|
||||
"isCrossRepository": false,
|
||||
"headRepositoryOwner": {"login":"OWNER"}
|
||||
},
|
||||
{
|
||||
"number": 13,
|
||||
"state": "OPEN",
|
||||
"baseRefName": "main",
|
||||
"headRefName": "blueberries",
|
||||
"isCrossRepository": false,
|
||||
"headRepositoryOwner": {"login":"OWNER"}
|
||||
}
|
||||
]}
|
||||
}}}`))
|
||||
},
|
||||
wantPR: 13,
|
||||
wantRepo: "https://github.com/OWNER/REPO",
|
||||
},
|
||||
{
|
||||
name: "branch argument with base branch",
|
||||
args: args{
|
||||
selector: "blueberries",
|
||||
baseBranch: "main",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("OWNER/REPO")
|
||||
},
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{
|
||||
"pullRequests":{"nodes":[
|
||||
{
|
||||
"number": 14,
|
||||
"state": "OPEN",
|
||||
"baseRefName": "dev",
|
||||
"headRefName": "blueberries",
|
||||
"isCrossRepository": false,
|
||||
"headRepositoryOwner": {"login":"OWNER"}
|
||||
},
|
||||
{
|
||||
"number": 13,
|
||||
"state": "OPEN",
|
||||
"baseRefName": "main",
|
||||
"headRefName": "blueberries",
|
||||
"isCrossRepository": false,
|
||||
"headRepositoryOwner": {"login":"OWNER"}
|
||||
}
|
||||
]}
|
||||
}}}`))
|
||||
},
|
||||
wantPR: 13,
|
||||
wantRepo: "https://github.com/OWNER/REPO",
|
||||
},
|
||||
{
|
||||
name: "no argument reads current branch",
|
||||
args: args{
|
||||
selector: "",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("OWNER/REPO")
|
||||
},
|
||||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
branchConfig: func(branch string) (c git.BranchConfig) {
|
||||
return
|
||||
},
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{
|
||||
"pullRequests":{"nodes":[
|
||||
{
|
||||
"number": 13,
|
||||
"state": "OPEN",
|
||||
"baseRefName": "main",
|
||||
"headRefName": "blueberries",
|
||||
"isCrossRepository": false,
|
||||
"headRepositoryOwner": {"login":"OWNER"}
|
||||
}
|
||||
]}
|
||||
}}}`))
|
||||
},
|
||||
wantPR: 13,
|
||||
wantRepo: "https://github.com/OWNER/REPO",
|
||||
},
|
||||
{
|
||||
name: "current branch is error",
|
||||
args: args{
|
||||
selector: "",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("OWNER/REPO")
|
||||
},
|
||||
branchFn: func() (string, error) {
|
||||
return "", errors.New("branchErr")
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "current branch with upstream configuration",
|
||||
args: args{
|
||||
selector: "",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("OWNER/REPO")
|
||||
},
|
||||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
branchConfig: func(branch string) (c git.BranchConfig) {
|
||||
c.MergeRef = "refs/heads/blue-upstream-berries"
|
||||
c.RemoteName = "origin"
|
||||
return
|
||||
},
|
||||
remotesFn: func() (context.Remotes, error) {
|
||||
return context.Remotes{{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
Repo: ghrepo.New("UPSTREAMOWNER", "REPO"),
|
||||
}}, nil
|
||||
},
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{
|
||||
"pullRequests":{"nodes":[
|
||||
{
|
||||
"number": 13,
|
||||
"state": "OPEN",
|
||||
"baseRefName": "main",
|
||||
"headRefName": "blue-upstream-berries",
|
||||
"isCrossRepository": true,
|
||||
"headRepositoryOwner": {"login":"UPSTREAMOWNER"}
|
||||
}
|
||||
]}
|
||||
}}}`))
|
||||
},
|
||||
wantPR: 13,
|
||||
wantRepo: "https://github.com/OWNER/REPO",
|
||||
},
|
||||
{
|
||||
name: "current branch with upstream configuration",
|
||||
args: args{
|
||||
selector: "",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("OWNER/REPO")
|
||||
},
|
||||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
branchConfig: func(branch string) (c git.BranchConfig) {
|
||||
u, _ := url.Parse("https://github.com/UPSTREAMOWNER/REPO")
|
||||
c.MergeRef = "refs/heads/blue-upstream-berries"
|
||||
c.RemoteURL = u
|
||||
return
|
||||
},
|
||||
remotesFn: nil,
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
httpmock.GraphQL(`query PullRequestForBranch\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{
|
||||
"pullRequests":{"nodes":[
|
||||
{
|
||||
"number": 13,
|
||||
"state": "OPEN",
|
||||
"baseRefName": "main",
|
||||
"headRefName": "blue-upstream-berries",
|
||||
"isCrossRepository": true,
|
||||
"headRepositoryOwner": {"login":"UPSTREAMOWNER"}
|
||||
}
|
||||
]}
|
||||
}}}`))
|
||||
},
|
||||
wantPR: 13,
|
||||
wantRepo: "https://github.com/OWNER/REPO",
|
||||
},
|
||||
{
|
||||
name: "current branch made by pr checkout",
|
||||
args: args{
|
||||
selector: "",
|
||||
fields: []string{"id", "number"},
|
||||
baseRepoFn: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("OWNER/REPO")
|
||||
},
|
||||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
branchConfig: func(branch string) (c git.BranchConfig) {
|
||||
c.MergeRef = "refs/pull/13/head"
|
||||
return
|
||||
},
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{
|
||||
"pullRequest":{"number":13}
|
||||
}}}`))
|
||||
},
|
||||
wantPR: 13,
|
||||
wantRepo: "https://github.com/OWNER/REPO",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
if tt.httpStub != nil {
|
||||
tt.httpStub(reg)
|
||||
}
|
||||
|
||||
f := finder{
|
||||
httpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
},
|
||||
baseRepoFn: tt.args.baseRepoFn,
|
||||
branchFn: tt.args.branchFn,
|
||||
branchConfig: tt.args.branchConfig,
|
||||
remotesFn: tt.args.remotesFn,
|
||||
}
|
||||
|
||||
pr, repo, err := f.Find(FindOptions{
|
||||
Selector: tt.args.selector,
|
||||
Fields: tt.args.fields,
|
||||
BaseBranch: tt.args.baseBranch,
|
||||
})
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Find() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if tt.wantErr {
|
||||
if tt.wantPR > 0 {
|
||||
t.Error("wantPR field is not checked in error case")
|
||||
}
|
||||
if tt.wantRepo != "" {
|
||||
t.Error("wantRepo field is not checked in error case")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if pr.Number != tt.wantPR {
|
||||
t.Errorf("want pr #%d, got #%d", tt.wantPR, pr.Number)
|
||||
}
|
||||
repoURL := ghrepo.GenerateRepoURL(repo, "")
|
||||
if repoURL != tt.wantRepo {
|
||||
t.Errorf("want repo %s, got %s", tt.wantRepo, repoURL)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
)
|
||||
|
||||
// PRFromArgs looks up the pull request from either the number/branch/URL argument or one belonging to the current branch
|
||||
//
|
||||
// NOTE: this API isn't great, but is here as a compatibility layer between old-style and new-style commands
|
||||
func PRFromArgs(apiClient *api.Client, baseRepoFn func() (ghrepo.Interface, error), branchFn func() (string, error), remotesFn func() (context.Remotes, error), arg string) (*api.PullRequest, ghrepo.Interface, error) {
|
||||
if arg != "" {
|
||||
// First check to see if the prString is a url, return repo from url if found. This
|
||||
// is run first because we don't need to run determineBaseRepo for this path
|
||||
pr, r, err := prFromURL(apiClient, arg)
|
||||
if pr != nil || err != nil {
|
||||
return pr, r, err
|
||||
}
|
||||
}
|
||||
|
||||
repo, err := baseRepoFn()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not determine base repo: %w", err)
|
||||
}
|
||||
|
||||
// If there are no args see if we can guess the PR from the current branch
|
||||
if arg == "" {
|
||||
pr, err := prForCurrentBranch(apiClient, repo, branchFn, remotesFn)
|
||||
return pr, repo, err
|
||||
} else {
|
||||
// Next see if the prString is a number and use that to look up the url
|
||||
pr, err := prFromNumberString(apiClient, repo, arg)
|
||||
if pr != nil || err != nil {
|
||||
return pr, repo, err
|
||||
}
|
||||
|
||||
// Last see if it is a branch name
|
||||
pr, err = api.PullRequestForBranch(apiClient, repo, "", arg, nil)
|
||||
return pr, repo, err
|
||||
}
|
||||
}
|
||||
|
||||
func prFromNumberString(apiClient *api.Client, repo ghrepo.Interface, s string) (*api.PullRequest, error) {
|
||||
if prNumber, err := strconv.Atoi(strings.TrimPrefix(s, "#")); err == nil {
|
||||
return api.PullRequestByNumber(apiClient, repo, prNumber)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var pullURLRE = regexp.MustCompile(`^/([^/]+)/([^/]+)/pull/(\d+)`)
|
||||
|
||||
func prFromURL(apiClient *api.Client, s string) (*api.PullRequest, ghrepo.Interface, error) {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
if u.Scheme != "https" && u.Scheme != "http" {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
m := pullURLRE.FindStringSubmatch(u.Path)
|
||||
if m == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
repo := ghrepo.NewWithHost(m[1], m[2], u.Hostname())
|
||||
prNumberString := m[3]
|
||||
pr, err := prFromNumberString(apiClient, repo, prNumberString)
|
||||
return pr, repo, err
|
||||
}
|
||||
|
||||
func prForCurrentBranch(apiClient *api.Client, repo ghrepo.Interface, branchFn func() (string, error), remotesFn func() (context.Remotes, error)) (*api.PullRequest, error) {
|
||||
prHeadRef, err := branchFn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
branchConfig := git.ReadBranchConfig(prHeadRef)
|
||||
|
||||
// the branch is configured to merge a special PR head ref
|
||||
prHeadRE := regexp.MustCompile(`^refs/pull/(\d+)/head$`)
|
||||
if m := prHeadRE.FindStringSubmatch(branchConfig.MergeRef); m != nil {
|
||||
return prFromNumberString(apiClient, repo, m[1])
|
||||
}
|
||||
|
||||
var branchOwner string
|
||||
if branchConfig.RemoteURL != nil {
|
||||
// the branch merges from a remote specified by URL
|
||||
if r, err := ghrepo.FromURL(branchConfig.RemoteURL); err == nil {
|
||||
branchOwner = r.RepoOwner()
|
||||
}
|
||||
} else if branchConfig.RemoteName != "" {
|
||||
// the branch merges from a remote specified by name
|
||||
rem, _ := remotesFn()
|
||||
if r, err := rem.FindByName(branchConfig.RemoteName); err == nil {
|
||||
branchOwner = r.RepoOwner()
|
||||
}
|
||||
}
|
||||
|
||||
if branchOwner != "" {
|
||||
if strings.HasPrefix(branchConfig.MergeRef, "refs/heads/") {
|
||||
prHeadRef = strings.TrimPrefix(branchConfig.MergeRef, "refs/heads/")
|
||||
}
|
||||
// prepend `OWNER:` if this branch is pushed to a fork
|
||||
if !strings.EqualFold(branchOwner, repo.RepoOwner()) {
|
||||
prHeadRef = fmt.Sprintf("%s:%s", branchOwner, prHeadRef)
|
||||
}
|
||||
}
|
||||
|
||||
return api.PullRequestForBranch(apiClient, repo, "", prHeadRef, nil)
|
||||
}
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
)
|
||||
|
||||
func TestPRFromArgs(t *testing.T) {
|
||||
type args struct {
|
||||
baseRepoFn func() (ghrepo.Interface, error)
|
||||
branchFn func() (string, error)
|
||||
remotesFn func() (context.Remotes, error)
|
||||
selector string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
httpStub func(*httpmock.Registry)
|
||||
wantPR int
|
||||
wantRepo string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "number argument",
|
||||
args: args{
|
||||
selector: "13",
|
||||
baseRepoFn: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("OWNER/REPO")
|
||||
},
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{
|
||||
"pullRequest":{"number":13}
|
||||
}}}`))
|
||||
},
|
||||
wantPR: 13,
|
||||
wantRepo: "https://github.com/OWNER/REPO",
|
||||
},
|
||||
{
|
||||
name: "number with hash argument",
|
||||
args: args{
|
||||
selector: "#13",
|
||||
baseRepoFn: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("OWNER/REPO")
|
||||
},
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{
|
||||
"pullRequest":{"number":13}
|
||||
}}}`))
|
||||
},
|
||||
wantPR: 13,
|
||||
wantRepo: "https://github.com/OWNER/REPO",
|
||||
},
|
||||
{
|
||||
name: "URL argument",
|
||||
args: args{
|
||||
selector: "https://example.org/OWNER/REPO/pull/13/files",
|
||||
baseRepoFn: nil,
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
httpmock.GraphQL(`query PullRequest_fields\b`),
|
||||
httpmock.StringResponse(`{"data":{}}`))
|
||||
r.Register(
|
||||
httpmock.GraphQL(`query PullRequest_fields2\b`),
|
||||
httpmock.StringResponse(`{"data":{}}`))
|
||||
r.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{
|
||||
"pullRequest":{"number":13}
|
||||
}}}`))
|
||||
},
|
||||
wantPR: 13,
|
||||
wantRepo: "https://example.org/OWNER/REPO",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
if tt.httpStub != nil {
|
||||
tt.httpStub(reg)
|
||||
}
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
pr, repo, err := PRFromArgs(api.NewClientFromHTTP(httpClient), tt.args.baseRepoFn, tt.args.branchFn, tt.args.remotesFn, tt.args.selector)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("IssueFromArg() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if pr.Number != tt.wantPR {
|
||||
t.Errorf("want issue #%d, got #%d", tt.wantPR, pr.Number)
|
||||
}
|
||||
repoURL := ghrepo.GenerateRepoURL(repo, "")
|
||||
if repoURL != tt.wantRepo {
|
||||
t.Errorf("want repo %s, got %s", tt.wantRepo, repoURL)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
|
|
@ -70,6 +69,8 @@ func Test_PreserveInput(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.state == nil {
|
||||
|
|
@ -78,9 +79,9 @@ func Test_PreserveInput(t *testing.T) {
|
|||
|
||||
io, _, _, errOut := iostreams.Test()
|
||||
|
||||
tf, tferr := tmpfile()
|
||||
tf, tferr := ioutil.TempFile(tempDir, "testfile*")
|
||||
assert.NoError(t, tferr)
|
||||
defer os.Remove(tf.Name())
|
||||
defer tf.Close()
|
||||
|
||||
io.TempFileOverride = tf
|
||||
|
||||
|
|
@ -111,13 +112,3 @@ func Test_PreserveInput(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func tmpfile() (*os.File, error) {
|
||||
dir := os.TempDir()
|
||||
tmpfile, err := ioutil.TempFile(dir, "testfile*")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tmpfile, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
"url": "https://github.com/cli/cli/pull/8",
|
||||
"headRefName": "strawberries",
|
||||
"reviewDecision": "CHANGES_REQUESTED",
|
||||
"commits": {
|
||||
"statusCheckRollup": {
|
||||
"nodes": [
|
||||
{
|
||||
"commit": {
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
"url": "https://github.com/cli/cli/pull/7",
|
||||
"headRefName": "banananana",
|
||||
"reviewDecision": "APPROVED",
|
||||
"commits": {
|
||||
"statusCheckRollup": {
|
||||
"nodes": [
|
||||
{
|
||||
"commit": {
|
||||
|
|
@ -72,7 +72,7 @@
|
|||
"url": "https://github.com/cli/cli/pull/6",
|
||||
"headRefName": "avo",
|
||||
"reviewDecision": "REVIEW_REQUIRED",
|
||||
"commits": {
|
||||
"statusCheckRollup": {
|
||||
"nodes": [
|
||||
{
|
||||
"commit": {
|
||||
|
|
|
|||
|
|
@ -113,13 +113,13 @@ func statusRun(opts *StatusOptions) error {
|
|||
if opts.Exporter != nil {
|
||||
data := map[string]interface{}{
|
||||
"currentBranch": nil,
|
||||
"createdBy": api.ExportPRs(prPayload.ViewerCreated.PullRequests, opts.Exporter.Fields()),
|
||||
"needsReview": api.ExportPRs(prPayload.ReviewRequested.PullRequests, opts.Exporter.Fields()),
|
||||
"createdBy": prPayload.ViewerCreated.PullRequests,
|
||||
"needsReview": prPayload.ReviewRequested.PullRequests,
|
||||
}
|
||||
if prPayload.CurrentPR != nil {
|
||||
data["currentBranch"] = prPayload.CurrentPR.ExportData(opts.Exporter.Fields())
|
||||
data["currentBranch"] = prPayload.CurrentPR
|
||||
}
|
||||
return opts.Exporter.Write(opts.IO.Out, &data, opts.IO.ColorEnabled())
|
||||
return opts.Exporter.Write(opts.IO.Out, data, opts.IO.ColorEnabled())
|
||||
}
|
||||
|
||||
out := opts.IO.Out
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
{
|
||||
"data": {
|
||||
"repository": {
|
||||
"pullRequests": {
|
||||
"nodes": [
|
||||
{
|
||||
"number": 12,
|
||||
"title": "Blueberries are from a fork",
|
||||
"state": "OPEN",
|
||||
"body": "yeah",
|
||||
"url": "https://github.com/OWNER/REPO/pull/12",
|
||||
"headRefName": "blueberries",
|
||||
"baseRefName": "master",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"additions": 100,
|
||||
"deletions": 10,
|
||||
"commits": {
|
||||
"totalCount": 12
|
||||
},
|
||||
"author": {
|
||||
"login": "nobody"
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"isDraft": false
|
||||
},
|
||||
{
|
||||
"number": 10,
|
||||
"title": "Blueberries are a good fruit",
|
||||
"state": "OPEN",
|
||||
"body": "**blueberries taste good**",
|
||||
"url": "https://github.com/OWNER/REPO/pull/10",
|
||||
"baseRefName": "master",
|
||||
"headRefName": "blueberries",
|
||||
"author": {
|
||||
"login": "nobody"
|
||||
},
|
||||
"additions": 100,
|
||||
"deletions": 10,
|
||||
"headRepositoryOwner": {
|
||||
"login": "OWNER"
|
||||
},
|
||||
"commits": {
|
||||
"totalCount": 8
|
||||
},
|
||||
"isCrossRepository": false,
|
||||
"isDraft": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
{
|
||||
"data": {
|
||||
"repository": {
|
||||
"pullRequests": {
|
||||
"nodes": [
|
||||
{
|
||||
"number": 12,
|
||||
"title": "Blueberries are from a fork",
|
||||
"state": "OPEN",
|
||||
"body": "yeah",
|
||||
"url": "https://github.com/OWNER/REPO/pull/12",
|
||||
"headRefName": "blueberries",
|
||||
"baseRefName": "master",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"additions": 100,
|
||||
"deletions": 10,
|
||||
"commits": {
|
||||
"totalCount": 12
|
||||
},
|
||||
"author": {
|
||||
"login": "nobody"
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"isDraft": false
|
||||
},
|
||||
{
|
||||
"number": 10,
|
||||
"title": "Blueberries are a good fruit",
|
||||
"state": "OPEN",
|
||||
"body": "**blueberries taste good**",
|
||||
"url": "https://github.com/OWNER/REPO/pull/10",
|
||||
"baseRefName": "master",
|
||||
"headRefName": "blueberries",
|
||||
"author": {
|
||||
"login": "nobody"
|
||||
},
|
||||
"headRepositoryOwner": {
|
||||
"login": "OWNER"
|
||||
},
|
||||
"additions": 100,
|
||||
"deletions": 10,
|
||||
"commits": {
|
||||
"totalCount": 8
|
||||
},
|
||||
"isCrossRepository": false,
|
||||
"isDraft": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
{ "data": { "repository": { "pullRequest": { "reviews": { } } } } }
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
{
|
||||
"data": {
|
||||
"repository": {
|
||||
"pullRequests": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "PR_12",
|
||||
"number": 12,
|
||||
"title": "Blueberries are from a fork",
|
||||
"state": "OPEN",
|
||||
"body": "yeah",
|
||||
"url": "https://github.com/OWNER/REPO/pull/12",
|
||||
"headRefName": "blueberries",
|
||||
"baseRefName": "master",
|
||||
"additions": 100,
|
||||
"deletions": 10,
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"assignees": {
|
||||
"nodes": [],
|
||||
"totalcount": 0
|
||||
},
|
||||
"labels": {
|
||||
"nodes": [],
|
||||
"totalcount": 0
|
||||
},
|
||||
"projectcards": {
|
||||
"nodes": [],
|
||||
"totalcount": 0
|
||||
},
|
||||
"milestone": {},
|
||||
"commits": {
|
||||
"totalCount": 12
|
||||
},
|
||||
"author": {
|
||||
"login": "nobody"
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"isDraft": false
|
||||
},
|
||||
{
|
||||
"id": "PR_10",
|
||||
"number": 10,
|
||||
"title": "Blueberries are a good fruit",
|
||||
"state": "OPEN",
|
||||
"body": "**blueberries taste good**",
|
||||
"url": "https://github.com/OWNER/REPO/pull/10",
|
||||
"baseRefName": "master",
|
||||
"headRefName": "blueberries",
|
||||
"author": {
|
||||
"login": "nobody"
|
||||
},
|
||||
"additions": 100,
|
||||
"deletions": 10,
|
||||
"assignees": {
|
||||
"nodes": [
|
||||
{
|
||||
"login": "marseilles"
|
||||
},
|
||||
{
|
||||
"login": "monaco"
|
||||
}
|
||||
],
|
||||
"totalcount": 2
|
||||
},
|
||||
"labels": {
|
||||
"nodes": [
|
||||
{
|
||||
"name": "one"
|
||||
},
|
||||
{
|
||||
"name": "two"
|
||||
},
|
||||
{
|
||||
"name": "three"
|
||||
},
|
||||
{
|
||||
"name": "four"
|
||||
},
|
||||
{
|
||||
"name": "five"
|
||||
}
|
||||
],
|
||||
"totalcount": 5
|
||||
},
|
||||
"projectcards": {
|
||||
"nodes": [
|
||||
{
|
||||
"project": {
|
||||
"name": "Project 1"
|
||||
},
|
||||
"column": {
|
||||
"name": "column A"
|
||||
}
|
||||
},
|
||||
{
|
||||
"project": {
|
||||
"name": "Project 2"
|
||||
},
|
||||
"column": {
|
||||
"name": "column B"
|
||||
}
|
||||
},
|
||||
{
|
||||
"project": {
|
||||
"name": "Project 3"
|
||||
},
|
||||
"column": {
|
||||
"name": "column C"
|
||||
}
|
||||
}
|
||||
],
|
||||
"totalcount": 3
|
||||
},
|
||||
"milestone": {
|
||||
"title": "uluru"
|
||||
},
|
||||
"headRepositoryOwner": {
|
||||
"login": "OWNER"
|
||||
},
|
||||
"commits": {
|
||||
"nodes": [
|
||||
{
|
||||
"commit": {
|
||||
"oid": "123456789"
|
||||
}
|
||||
}
|
||||
],
|
||||
"totalCount": 8
|
||||
},
|
||||
"isCrossRepository": false,
|
||||
"isDraft": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
{
|
||||
"data": {
|
||||
"repository": {
|
||||
"pullRequests": {
|
||||
"nodes": [
|
||||
{
|
||||
"number": 12,
|
||||
"title": "Blueberries are from a fork",
|
||||
"state": "OPEN",
|
||||
"body": "yeah",
|
||||
"url": "https://github.com/OWNER/REPO/pull/12",
|
||||
"headRefName": "blueberries",
|
||||
"baseRefName": "master",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"additions": 100,
|
||||
"deletions": 10,
|
||||
"commits": {
|
||||
"totalCount": 12
|
||||
},
|
||||
"author": {
|
||||
"login": "nobody"
|
||||
},
|
||||
"isCrossRepository": true
|
||||
},
|
||||
{
|
||||
"number": 10,
|
||||
"title": "Blueberries are a good fruit",
|
||||
"state": "OPEN",
|
||||
"body": "",
|
||||
"url": "https://github.com/OWNER/REPO/pull/10",
|
||||
"baseRefName": "master",
|
||||
"headRefName": "blueberries",
|
||||
"additions": 100,
|
||||
"deletions": 10,
|
||||
"author": {
|
||||
"login": "nobody"
|
||||
},
|
||||
"headRepositoryOwner": {
|
||||
"login": "OWNER"
|
||||
},
|
||||
"commits": {
|
||||
"totalCount": 8
|
||||
},
|
||||
"isCrossRepository": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
{"data":{
|
||||
"repository": {
|
||||
"pullRequests": {
|
||||
"edges": []
|
||||
}
|
||||
},
|
||||
"viewerCreated": {
|
||||
"edges": [],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
},
|
||||
"reviewRequested": {
|
||||
"edges": [],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
}
|
||||
}}
|
||||
|
|
@ -3,17 +3,12 @@ package view
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"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/iostreams"
|
||||
|
|
@ -27,14 +22,10 @@ type browser interface {
|
|||
}
|
||||
|
||||
type ViewOptions struct {
|
||||
HttpClient func() (*http.Client, error)
|
||||
Config func() (config.Config, error)
|
||||
IO *iostreams.IOStreams
|
||||
Browser browser
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Remotes func() (context.Remotes, error)
|
||||
Branch func() (string, error)
|
||||
IO *iostreams.IOStreams
|
||||
Browser browser
|
||||
|
||||
Finder shared.PRFinder
|
||||
Exporter cmdutil.Exporter
|
||||
|
||||
SelectorArg string
|
||||
|
|
@ -44,12 +35,8 @@ type ViewOptions struct {
|
|||
|
||||
func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Command {
|
||||
opts := &ViewOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
Config: f.Config,
|
||||
Remotes: f.Remotes,
|
||||
Branch: f.Branch,
|
||||
Browser: f.Browser,
|
||||
IO: f.IOStreams,
|
||||
Browser: f.Browser,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
|
@ -65,8 +52,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
|
|||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
opts.Finder = shared.NewFinder(f)
|
||||
|
||||
if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 {
|
||||
return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")}
|
||||
|
|
@ -90,10 +76,25 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
|
|||
return cmd
|
||||
}
|
||||
|
||||
var defaultFields = []string{
|
||||
"url", "number", "title", "state", "body", "author",
|
||||
"isDraft", "maintainerCanModify", "mergeable", "additions", "deletions", "commitsCount",
|
||||
"baseRefName", "headRefName", "headRepositoryOwner", "headRepository", "isCrossRepository",
|
||||
"reviewRequests", "reviews", "assignees", "labels", "projectCards", "milestone",
|
||||
"comments", "reactionGroups",
|
||||
}
|
||||
|
||||
func viewRun(opts *ViewOptions) error {
|
||||
opts.IO.StartProgressIndicator()
|
||||
pr, err := retrievePullRequest(opts)
|
||||
opts.IO.StopProgressIndicator()
|
||||
findOptions := shared.FindOptions{
|
||||
Selector: opts.SelectorArg,
|
||||
Fields: defaultFields,
|
||||
}
|
||||
if opts.BrowserMode {
|
||||
findOptions.Fields = []string{"url"}
|
||||
} else if opts.Exporter != nil {
|
||||
findOptions.Fields = opts.Exporter.Fields()
|
||||
}
|
||||
pr, _, err := opts.Finder.Find(findOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -117,8 +118,7 @@ func viewRun(opts *ViewOptions) error {
|
|||
defer opts.IO.StopPager()
|
||||
|
||||
if opts.Exporter != nil {
|
||||
exportPR := pr.ExportData(opts.Exporter.Fields())
|
||||
return opts.Exporter.Write(opts.IO.Out, exportPR, opts.IO.ColorEnabled())
|
||||
return opts.Exporter.Write(opts.IO.Out, pr, opts.IO.ColorEnabled())
|
||||
}
|
||||
|
||||
if connectedToTerminal {
|
||||
|
|
@ -149,7 +149,11 @@ func printRawPrPreview(io *iostreams.IOStreams, pr *api.PullRequest) error {
|
|||
fmt.Fprintf(out, "assignees:\t%s\n", assignees)
|
||||
fmt.Fprintf(out, "reviewers:\t%s\n", reviewers)
|
||||
fmt.Fprintf(out, "projects:\t%s\n", projects)
|
||||
fmt.Fprintf(out, "milestone:\t%s\n", pr.Milestone.Title)
|
||||
var milestoneTitle string
|
||||
if pr.Milestone != nil {
|
||||
milestoneTitle = pr.Milestone.Title
|
||||
}
|
||||
fmt.Fprintf(out, "milestone:\t%s\n", milestoneTitle)
|
||||
fmt.Fprintf(out, "number:\t%d\n", pr.Number)
|
||||
fmt.Fprintf(out, "url:\t%s\n", pr.URL)
|
||||
fmt.Fprintf(out, "additions:\t%s\n", cs.Green(strconv.Itoa(pr.Additions)))
|
||||
|
|
@ -201,7 +205,7 @@ func printHumanPrPreview(opts *ViewOptions, pr *api.PullRequest) error {
|
|||
fmt.Fprint(out, cs.Bold("Projects: "))
|
||||
fmt.Fprintln(out, projects)
|
||||
}
|
||||
if pr.Milestone.Title != "" {
|
||||
if pr.Milestone != nil {
|
||||
fmt.Fprint(out, cs.Bold("Milestone: "))
|
||||
fmt.Fprintln(out, pr.Milestone.Title)
|
||||
}
|
||||
|
|
@ -413,51 +417,3 @@ 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,16 +2,17 @@ package view
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/internal/run"
|
||||
"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"
|
||||
|
|
@ -121,26 +122,6 @@ func runCommand(rt http.RoundTripper, branch string, isTTY bool, cli string) (*t
|
|||
factory := &cmdutil.Factory{
|
||||
IOStreams: io,
|
||||
Browser: browser,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: rt}, nil
|
||||
},
|
||||
Config: func() (config.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
Remotes: func() (context.Remotes, error) {
|
||||
return context.Remotes{
|
||||
{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
Repo: ghrepo.New("OWNER", "REPO"),
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
Branch: func() (string, error) {
|
||||
return branch, nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd := NewCmdView(factory, nil)
|
||||
|
|
@ -163,6 +144,50 @@ func runCommand(rt http.RoundTripper, branch string, isTTY bool, cli string) (*t
|
|||
}, err
|
||||
}
|
||||
|
||||
// hack for compatibility with old JSON fixture files
|
||||
func prFromFixtures(fixtures map[string]string) (*api.PullRequest, error) {
|
||||
var response struct {
|
||||
Data struct {
|
||||
Repository struct {
|
||||
PullRequest *api.PullRequest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ff, err := os.Open(fixtures["PullRequestByNumber"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer ff.Close()
|
||||
|
||||
dec := json.NewDecoder(ff)
|
||||
err = dec.Decode(&response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for name := range fixtures {
|
||||
switch name {
|
||||
case "PullRequestByNumber":
|
||||
case "ReviewsForPullRequest", "CommentsForPullRequest":
|
||||
ff, err := os.Open(fixtures[name])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer ff.Close()
|
||||
dec := json.NewDecoder(ff)
|
||||
err = dec.Decode(&response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unrecognized fixture type: %q", name)
|
||||
}
|
||||
}
|
||||
|
||||
return response.Data.Repository.PullRequest, nil
|
||||
}
|
||||
|
||||
func TestPRView_Preview_nontty(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
branch string
|
||||
|
|
@ -174,8 +199,7 @@ func TestPRView_Preview_nontty(t *testing.T) {
|
|||
branch: "master",
|
||||
args: "12",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreview.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
"PullRequestByNumber": "./fixtures/prViewPreview.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`title:\tBlueberries are from a fork\n`,
|
||||
|
|
@ -197,8 +221,7 @@ func TestPRView_Preview_nontty(t *testing.T) {
|
|||
branch: "master",
|
||||
args: "12",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewWithMetadataByNumber.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewWithMetadataByNumber.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`title:\tBlueberries are from a fork\n`,
|
||||
|
|
@ -231,74 +254,11 @@ func TestPRView_Preview_nontty(t *testing.T) {
|
|||
`\*\*blueberries taste good\*\*`,
|
||||
},
|
||||
},
|
||||
"Open PR with metadata by branch": {
|
||||
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`,
|
||||
`additions:\t100\n`,
|
||||
`deletions:\t10\n`,
|
||||
`blueberries taste good`,
|
||||
},
|
||||
},
|
||||
"Open PR for the current branch": {
|
||||
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`,
|
||||
`additions:\t100\n`,
|
||||
`deletions:\t10\n`,
|
||||
`\*\*blueberries taste good\*\*`,
|
||||
},
|
||||
},
|
||||
"Open PR wth empty body for the current branch": {
|
||||
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`,
|
||||
`additions:\t100\n`,
|
||||
`deletions:\t10\n`,
|
||||
},
|
||||
},
|
||||
"Closed PR": {
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewClosedState.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewClosedState.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`state:\tCLOSED\n`,
|
||||
|
|
@ -317,8 +277,7 @@ func TestPRView_Preview_nontty(t *testing.T) {
|
|||
branch: "master",
|
||||
args: "12",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewMergedState.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewMergedState.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`state:\tMERGED\n`,
|
||||
|
|
@ -337,8 +296,7 @@ func TestPRView_Preview_nontty(t *testing.T) {
|
|||
branch: "master",
|
||||
args: "12",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewDraftState.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewDraftState.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`title:\tBlueberries are from a fork\n`,
|
||||
|
|
@ -354,37 +312,16 @@ func TestPRView_Preview_nontty(t *testing.T) {
|
|||
`\*\*blueberries taste good\*\*`,
|
||||
},
|
||||
},
|
||||
"Draft PR by branch": {
|
||||
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:`,
|
||||
`additions:\t100\n`,
|
||||
`deletions:\t10\n`,
|
||||
`\*\*blueberries taste good\*\*`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
for name, file := range tc.fixtures {
|
||||
name := fmt.Sprintf(`query %s\b`, name)
|
||||
http.Register(httpmock.GraphQL(name), httpmock.FileResponse(file))
|
||||
}
|
||||
|
||||
pr, err := prFromFixtures(tc.fixtures)
|
||||
require.NoError(t, err)
|
||||
shared.RunCommandFinder("12", pr, ghrepo.New("OWNER", "REPO"))
|
||||
|
||||
output, err := runCommand(http, tc.branch, false, tc.args)
|
||||
if err != nil {
|
||||
|
|
@ -410,8 +347,7 @@ func TestPRView_Preview(t *testing.T) {
|
|||
branch: "master",
|
||||
args: "12",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreview.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
"PullRequestByNumber": "./fixtures/prViewPreview.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`Blueberries are from a fork`,
|
||||
|
|
@ -424,8 +360,7 @@ func TestPRView_Preview(t *testing.T) {
|
|||
branch: "master",
|
||||
args: "12",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewWithMetadataByNumber.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewWithMetadataByNumber.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`Blueberries are from a fork`,
|
||||
|
|
@ -453,57 +388,11 @@ func TestPRView_Preview(t *testing.T) {
|
|||
`View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12`,
|
||||
},
|
||||
},
|
||||
"Open PR with metadata by branch": {
|
||||
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.+100.-10`,
|
||||
`Assignees:.*marseilles, monaco\n`,
|
||||
`Labels:.*one, two, three, four, five\n`,
|
||||
`Projects:.*Project 1 \(column A\), Project 2 \(column B\), Project 3 \(column C\)\n`,
|
||||
`Milestone:.*uluru\n`,
|
||||
`blueberries taste good`,
|
||||
`View this pull request on GitHub: https://github.com/OWNER/REPO/pull/10`,
|
||||
},
|
||||
},
|
||||
"Open PR for the current branch": {
|
||||
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.+100.-10`,
|
||||
`blueberries taste good`,
|
||||
`View this pull request on GitHub: https://github.com/OWNER/REPO/pull/10`,
|
||||
},
|
||||
},
|
||||
"Open PR wth empty body for the current branch": {
|
||||
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.+100.-10`,
|
||||
`View this pull request on GitHub: https://github.com/OWNER/REPO/pull/10`,
|
||||
},
|
||||
},
|
||||
"Closed PR": {
|
||||
branch: "master",
|
||||
args: "12",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewClosedState.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewClosedState.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`Blueberries are from a fork`,
|
||||
|
|
@ -516,8 +405,7 @@ func TestPRView_Preview(t *testing.T) {
|
|||
branch: "master",
|
||||
args: "12",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewMergedState.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewMergedState.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`Blueberries are from a fork`,
|
||||
|
|
@ -530,8 +418,7 @@ func TestPRView_Preview(t *testing.T) {
|
|||
branch: "master",
|
||||
args: "12",
|
||||
fixtures: map[string]string{
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewDraftState.json",
|
||||
"ReviewsForPullRequest": "./fixtures/prViewPreviewNoReviews.json",
|
||||
"PullRequestByNumber": "./fixtures/prViewPreviewDraftState.json",
|
||||
},
|
||||
expectedOutputs: []string{
|
||||
`Blueberries are from a fork`,
|
||||
|
|
@ -540,30 +427,16 @@ func TestPRView_Preview(t *testing.T) {
|
|||
`View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12`,
|
||||
},
|
||||
},
|
||||
"Draft PR by branch": {
|
||||
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.+100.-10`,
|
||||
`blueberries taste good`,
|
||||
`View this pull request on GitHub: https://github.com/OWNER/REPO/pull/10`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
for name, file := range tc.fixtures {
|
||||
name := fmt.Sprintf(`query %s\b`, name)
|
||||
http.Register(httpmock.GraphQL(name), httpmock.FileResponse(file))
|
||||
}
|
||||
|
||||
pr, err := prFromFixtures(tc.fixtures)
|
||||
require.NoError(t, err)
|
||||
shared.RunCommandFinder("12", pr, ghrepo.New("OWNER", "REPO"))
|
||||
|
||||
output, err := runCommand(http, tc.branch, true, tc.args)
|
||||
if err != nil {
|
||||
|
|
@ -581,13 +454,12 @@ func TestPRView_Preview(t *testing.T) {
|
|||
func TestPRView_web_currentBranch(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
http.Register(httpmock.GraphQL(`query PullRequestForBranch\b`), httpmock.FileResponse("./fixtures/prView.json"))
|
||||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
shared.RunCommandFinder("", &api.PullRequest{URL: "https://github.com/OWNER/REPO/pull/10"}, ghrepo.New("OWNER", "REPO"))
|
||||
|
||||
_, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
cs.Register(`git config --get-regexp.+branch\\\.blueberries\\\.`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "-w")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `pr view`: %v", err)
|
||||
|
|
@ -601,41 +473,16 @@ func TestPRView_web_currentBranch(t *testing.T) {
|
|||
func TestPRView_web_noResultsForBranch(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
http.Register(httpmock.GraphQL(`query PullRequestForBranch\b`), httpmock.FileResponse("./fixtures/prView_NoActiveBranch.json"))
|
||||
|
||||
cs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
cs.Register(`git config --get-regexp.+branch\\\.blueberries\\\.`, 0, "")
|
||||
|
||||
_, err := runCommand(http, "blueberries", true, "-w")
|
||||
if err == nil || err.Error() != `no pull requests found for branch "blueberries"` {
|
||||
t.Errorf("error running command `pr view`: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRView_web_numberArg(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`query PullRequestByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"url": "https://github.com/OWNER/REPO/pull/23"
|
||||
} } } }`),
|
||||
)
|
||||
shared.RunCommandFinder("", nil, nil)
|
||||
|
||||
_, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
output, err := runCommand(http, "master", true, "-w 23")
|
||||
if err != nil {
|
||||
_, err := runCommand(http, "blueberries", true, "-w")
|
||||
if err == nil || err.Error() != `no pull requests found` {
|
||||
t.Errorf("error running command `pr view`: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/pull/23", output.BrowsedURL)
|
||||
}
|
||||
|
||||
func TestPRView_tty_Comments(t *testing.T) {
|
||||
|
|
@ -715,10 +562,15 @@ func TestPRView_tty_Comments(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
for name, file := range tt.fixtures {
|
||||
name := fmt.Sprintf(`query %s\b`, name)
|
||||
http.Register(httpmock.GraphQL(name), httpmock.FileResponse(file))
|
||||
|
||||
if len(tt.fixtures) > 0 {
|
||||
pr, err := prFromFixtures(tt.fixtures)
|
||||
require.NoError(t, err)
|
||||
shared.RunCommandFinder("123", pr, ghrepo.New("OWNER", "REPO"))
|
||||
} else {
|
||||
shared.RunCommandFinder("123", nil, nil)
|
||||
}
|
||||
|
||||
output, err := runCommand(http, tt.branch, true, tt.cli)
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
|
|
@ -821,10 +673,15 @@ func TestPRView_nontty_Comments(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
http := &httpmock.Registry{}
|
||||
defer http.Verify(t)
|
||||
for name, file := range tt.fixtures {
|
||||
name := fmt.Sprintf(`query %s\b`, name)
|
||||
http.Register(httpmock.GraphQL(name), httpmock.FileResponse(file))
|
||||
|
||||
if len(tt.fixtures) > 0 {
|
||||
pr, err := prFromFixtures(tt.fixtures)
|
||||
require.NoError(t, err)
|
||||
shared.RunCommandFinder("123", pr, ghrepo.New("OWNER", "REPO"))
|
||||
} else {
|
||||
shared.RunCommandFinder("123", nil, nil)
|
||||
}
|
||||
|
||||
output, err := runCommand(http, tt.branch, false, tt.cli)
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
|
|
|
|||
|
|
@ -313,7 +313,7 @@ func createRun(opts *CreateOptions) error {
|
|||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(opts.IO.Out, "%s\n", newRelease.HTMLURL)
|
||||
fmt.Fprintf(opts.IO.Out, "%s\n", newRelease.URL)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
|
|
@ -13,30 +15,106 @@ import (
|
|||
"github.com/cli/cli/internal/ghrepo"
|
||||
)
|
||||
|
||||
type Release struct {
|
||||
TagName string `json:"tag_name"`
|
||||
Name string `json:"name"`
|
||||
Body string `json:"body"`
|
||||
IsDraft bool `json:"draft"`
|
||||
IsPrerelease bool `json:"prerelease"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
PublishedAt time.Time `json:"published_at"`
|
||||
var ReleaseFields = []string{
|
||||
"url",
|
||||
"apiUrl",
|
||||
"uploadUrl",
|
||||
"tarballUrl",
|
||||
"zipballUrl",
|
||||
"id",
|
||||
"tagName",
|
||||
"name",
|
||||
"body",
|
||||
"isDraft",
|
||||
"isPrerelease",
|
||||
"createdAt",
|
||||
"publishedAt",
|
||||
"targetCommitish",
|
||||
"author",
|
||||
"assets",
|
||||
}
|
||||
|
||||
APIURL string `json:"url"`
|
||||
UploadURL string `json:"upload_url"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
Assets []ReleaseAsset
|
||||
type Release struct {
|
||||
ID string `json:"node_id"`
|
||||
TagName string `json:"tag_name"`
|
||||
Name string `json:"name"`
|
||||
Body string `json:"body"`
|
||||
IsDraft bool `json:"draft"`
|
||||
IsPrerelease bool `json:"prerelease"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
PublishedAt *time.Time `json:"published_at"`
|
||||
|
||||
TargetCommitish string `json:"target_commitish"`
|
||||
|
||||
APIURL string `json:"url"`
|
||||
UploadURL string `json:"upload_url"`
|
||||
TarballURL string `json:"tarball_url"`
|
||||
ZipballURL string `json:"zipball_url"`
|
||||
URL string `json:"html_url"`
|
||||
Assets []ReleaseAsset
|
||||
|
||||
Author struct {
|
||||
Login string
|
||||
ID string `json:"node_id"`
|
||||
Login string `json:"login"`
|
||||
}
|
||||
}
|
||||
|
||||
type ReleaseAsset struct {
|
||||
ID string `json:"node_id"`
|
||||
Name string
|
||||
Label string
|
||||
Size int64
|
||||
State string
|
||||
APIURL string `json:"url"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DownloadCount int `json:"download_count"`
|
||||
ContentType string `json:"content_type"`
|
||||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
}
|
||||
|
||||
func (rel *Release) ExportData(fields []string) *map[string]interface{} {
|
||||
v := reflect.ValueOf(rel).Elem()
|
||||
fieldByName := func(v reflect.Value, field string) reflect.Value {
|
||||
return v.FieldByNameFunc(func(s string) bool {
|
||||
return strings.EqualFold(field, s)
|
||||
})
|
||||
}
|
||||
data := map[string]interface{}{}
|
||||
|
||||
for _, f := range fields {
|
||||
switch f {
|
||||
case "author":
|
||||
data[f] = map[string]interface{}{
|
||||
"id": rel.Author.ID,
|
||||
"login": rel.Author.Login,
|
||||
}
|
||||
case "assets":
|
||||
assets := make([]interface{}, 0, len(rel.Assets))
|
||||
for _, a := range rel.Assets {
|
||||
assets = append(assets, map[string]interface{}{
|
||||
"url": a.BrowserDownloadURL,
|
||||
"apiUrl": a.APIURL,
|
||||
"id": a.ID,
|
||||
"name": a.Name,
|
||||
"label": a.Label,
|
||||
"size": a.Size,
|
||||
"state": a.State,
|
||||
"createdAt": a.CreatedAt,
|
||||
"updatedAt": a.UpdatedAt,
|
||||
"downloadCount": a.DownloadCount,
|
||||
"contentType": a.ContentType,
|
||||
})
|
||||
}
|
||||
data[f] = assets
|
||||
default:
|
||||
sf := fieldByName(v, f)
|
||||
data[f] = sf.Interface()
|
||||
}
|
||||
}
|
||||
|
||||
return &data
|
||||
}
|
||||
|
||||
// FetchRelease finds a repository release by its tagName.
|
||||
|
|
@ -55,11 +133,7 @@ func FetchRelease(httpClient *http.Client, baseRepo ghrepo.Interface, tagName st
|
|||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
if canPush, err := api.CanPushToRepo(httpClient, baseRepo); err == nil && canPush {
|
||||
return FindDraftRelease(httpClient, baseRepo, tagName)
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return FindDraftRelease(httpClient, baseRepo, tagName)
|
||||
}
|
||||
|
||||
if resp.StatusCode > 299 {
|
||||
|
|
@ -152,11 +226,8 @@ func FindDraftRelease(httpClient *http.Client, baseRepo ghrepo.Interface, tagNam
|
|||
return &r, nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(releases) < perPage {
|
||||
break
|
||||
}
|
||||
page++
|
||||
//nolint:staticcheck
|
||||
break
|
||||
}
|
||||
|
||||
return nil, errors.New("release not found")
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ type ViewOptions struct {
|
|||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Browser browser
|
||||
Exporter cmdutil.Exporter
|
||||
|
||||
TagName string
|
||||
WebMode bool
|
||||
|
|
@ -64,6 +65,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
|
|||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "Open the release in the browser")
|
||||
cmdutil.AddJSONFlags(cmd, &opts.Exporter, shared.ReleaseFields)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -95,9 +97,19 @@ func viewRun(opts *ViewOptions) error {
|
|||
|
||||
if opts.WebMode {
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(release.HTMLURL))
|
||||
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(release.URL))
|
||||
}
|
||||
return opts.Browser.Browse(release.HTMLURL)
|
||||
return opts.Browser.Browse(release.URL)
|
||||
}
|
||||
|
||||
opts.IO.DetectTerminalTheme()
|
||||
if err := opts.IO.StartPager(); err != nil {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "error starting pager: %v\n", err)
|
||||
}
|
||||
defer opts.IO.StopPager()
|
||||
|
||||
if opts.Exporter != nil {
|
||||
return opts.Exporter.Write(opts.IO.Out, release, opts.IO.ColorEnabled())
|
||||
}
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
|
|
@ -126,10 +138,10 @@ func renderReleaseTTY(io *iostreams.IOStreams, release *shared.Release) error {
|
|||
if release.IsDraft {
|
||||
fmt.Fprintf(w, "%s\n", iofmt.Gray(fmt.Sprintf("%s created this %s", release.Author.Login, utils.FuzzyAgo(time.Since(release.CreatedAt)))))
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\n", iofmt.Gray(fmt.Sprintf("%s released this %s", release.Author.Login, utils.FuzzyAgo(time.Since(release.PublishedAt)))))
|
||||
fmt.Fprintf(w, "%s\n", iofmt.Gray(fmt.Sprintf("%s released this %s", release.Author.Login, utils.FuzzyAgo(time.Since(*release.PublishedAt)))))
|
||||
}
|
||||
|
||||
style := markdown.GetStyle(io.DetectTerminalTheme())
|
||||
style := markdown.GetStyle(io.TerminalTheme())
|
||||
renderedDescription, err := markdown.Render(release.Body, style)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -151,7 +163,7 @@ func renderReleaseTTY(io *iostreams.IOStreams, release *shared.Release) error {
|
|||
fmt.Fprint(w, "\n")
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "%s\n", iofmt.Gray(fmt.Sprintf("View on GitHub: %s", release.HTMLURL)))
|
||||
fmt.Fprintf(w, "%s\n", iofmt.Gray(fmt.Sprintf("View on GitHub: %s", release.URL)))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -165,7 +177,7 @@ func renderReleasePlain(w io.Writer, release *shared.Release) error {
|
|||
if !release.IsDraft {
|
||||
fmt.Fprintf(w, "published:\t%s\n", release.PublishedAt.Format(time.RFC3339))
|
||||
}
|
||||
fmt.Fprintf(w, "url:\t%s\n", release.HTMLURL)
|
||||
fmt.Fprintf(w, "url:\t%s\n", release.URL)
|
||||
for _, a := range release.Assets {
|
||||
fmt.Fprintf(w, "asset:\t%s\n", a.Name)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ type ForkOptions struct {
|
|||
PromptClone bool
|
||||
PromptRemote bool
|
||||
RemoteName string
|
||||
Organization string
|
||||
Rename bool
|
||||
}
|
||||
|
||||
|
|
@ -110,6 +111,7 @@ Additional 'git clone' flags can be passed in by listing them after '--'.`,
|
|||
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.")
|
||||
cmd.Flags().StringVar(&opts.Organization, "org", "", "Create the fork in an organization")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -169,7 +171,7 @@ func forkRun(opts *ForkOptions) error {
|
|||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
opts.IO.StartProgressIndicator()
|
||||
forkedRepo, err := api.ForkRepo(apiClient, repoToFork)
|
||||
forkedRepo, err := api.ForkRepo(apiClient, repoToFork, opts.Organization)
|
||||
opts.IO.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fork: %w", err)
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@ package fork
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/config"
|
||||
|
|
@ -72,8 +74,9 @@ func TestNewCmdFork(t *testing.T) {
|
|||
name: "blank nontty",
|
||||
cli: "",
|
||||
wants: ForkOptions{
|
||||
RemoteName: "origin",
|
||||
Rename: true,
|
||||
RemoteName: "origin",
|
||||
Rename: true,
|
||||
Organization: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -85,6 +88,7 @@ func TestNewCmdFork(t *testing.T) {
|
|||
PromptClone: true,
|
||||
PromptRemote: true,
|
||||
Rename: true,
|
||||
Organization: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -104,6 +108,16 @@ func TestNewCmdFork(t *testing.T) {
|
|||
Rename: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "to org",
|
||||
cli: "--org batmanshome",
|
||||
wants: ForkOptions{
|
||||
RemoteName: "origin",
|
||||
Remote: false,
|
||||
Rename: false,
|
||||
Organization: "batmanshome",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -141,6 +155,7 @@ func TestNewCmdFork(t *testing.T) {
|
|||
assert.Equal(t, tt.wants.Remote, gotOpts.Remote)
|
||||
assert.Equal(t, tt.wants.PromptRemote, gotOpts.PromptRemote)
|
||||
assert.Equal(t, tt.wants.PromptClone, gotOpts.PromptClone)
|
||||
assert.Equal(t, tt.wants.Organization, gotOpts.Organization)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -289,6 +304,7 @@ func TestRepoFork_in_parent_tty(t *testing.T) {
|
|||
assert.Equal(t, "✓ Created fork someone/REPO\n✓ Added remote origin\n", output.Stderr())
|
||||
reg.Verify(t)
|
||||
}
|
||||
|
||||
func TestRepoFork_in_parent_nontty(t *testing.T) {
|
||||
defer stubSince(2 * time.Second)()
|
||||
reg := &httpmock.Registry{}
|
||||
|
|
@ -409,37 +425,65 @@ func TestRepoFork_in_parent(t *testing.T) {
|
|||
|
||||
func TestRepoFork_outside(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args string
|
||||
name string
|
||||
args string
|
||||
postBody string
|
||||
responseBody string
|
||||
wantStderr string
|
||||
}{
|
||||
{
|
||||
name: "url arg",
|
||||
args: "--clone=false http://github.com/OWNER/REPO.git",
|
||||
name: "url arg",
|
||||
args: "--clone=false http://github.com/OWNER/REPO.git",
|
||||
postBody: "{}\n",
|
||||
responseBody: `{"name":"REPO", "owner":{"login":"monalisa"}}`,
|
||||
wantStderr: heredoc.Doc(`
|
||||
✓ Created fork monalisa/REPO
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "full name arg",
|
||||
args: "--clone=false OWNER/REPO",
|
||||
name: "full name arg",
|
||||
args: "--clone=false OWNER/REPO",
|
||||
postBody: "{}\n",
|
||||
responseBody: `{"name":"REPO", "owner":{"login":"monalisa"}}`,
|
||||
wantStderr: heredoc.Doc(`
|
||||
✓ Created fork monalisa/REPO
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "fork to org without clone",
|
||||
args: "--clone=false OWNER/REPO --org batmanshome",
|
||||
postBody: "{\"organization\":\"batmanshome\"}\n",
|
||||
responseBody: `{"name":"REPO", "owner":{"login":"BatmansHome"}}`,
|
||||
wantStderr: heredoc.Doc(`
|
||||
✓ Created fork BatmansHome/REPO
|
||||
`),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer stubSince(2 * time.Second)()
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
reg.Register(
|
||||
httpmock.REST("POST", "repos/OWNER/REPO/forks"),
|
||||
func(req *http.Request) (*http.Response, error) {
|
||||
bb, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
assert.Equal(t, tt.postBody, string(bb))
|
||||
return &http.Response{
|
||||
Request: req,
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(bytes.NewBufferString(tt.responseBody)),
|
||||
}, nil
|
||||
})
|
||||
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
output, err := runCommand(httpClient, nil, true, tt.args)
|
||||
if err != nil {
|
||||
t.Errorf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
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
|
||||
}
|
||||
assert.Equal(t, tt.wantStderr, output.Stderr())
|
||||
reg.Verify(t)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +1,18 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/internal/ghinstance"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/pkg/githubsearch"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/shurcooL/graphql"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
NameWithOwner string
|
||||
Description string
|
||||
IsFork bool
|
||||
IsPrivate bool
|
||||
IsArchived bool
|
||||
PushedAt time.Time
|
||||
}
|
||||
|
||||
func (r Repository) Info() string {
|
||||
var tags []string
|
||||
|
||||
if r.IsPrivate {
|
||||
tags = append(tags, "private")
|
||||
} else {
|
||||
tags = append(tags, "public")
|
||||
}
|
||||
if r.IsFork {
|
||||
tags = append(tags, "fork")
|
||||
}
|
||||
if r.IsArchived {
|
||||
tags = append(tags, "archived")
|
||||
}
|
||||
|
||||
return strings.Join(tags, ", ")
|
||||
}
|
||||
|
||||
type RepositoryList struct {
|
||||
Owner string
|
||||
Repositories []Repository
|
||||
Repositories []api.Repository
|
||||
TotalCount int
|
||||
FromSearch bool
|
||||
}
|
||||
|
|
@ -54,6 +24,7 @@ type FilterOptions struct {
|
|||
Language string
|
||||
Archived bool
|
||||
NonArchived bool
|
||||
Fields []string
|
||||
}
|
||||
|
||||
func listRepos(client *http.Client, hostname string, limit int, owner string, filter FilterOptions) (*RepositoryList, error) {
|
||||
|
|
@ -67,62 +38,65 @@ func listRepos(client *http.Client, hostname string, limit int, owner string, fi
|
|||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"perPage": githubv4.Int(perPage),
|
||||
"endCursor": (*githubv4.String)(nil),
|
||||
"perPage": githubv4.Int(perPage),
|
||||
}
|
||||
|
||||
if filter.Visibility != "" {
|
||||
variables["privacy"] = githubv4.RepositoryPrivacy(strings.ToUpper(filter.Visibility))
|
||||
} else {
|
||||
variables["privacy"] = (*githubv4.RepositoryPrivacy)(nil)
|
||||
}
|
||||
|
||||
if filter.Fork {
|
||||
variables["fork"] = githubv4.Boolean(true)
|
||||
} else if filter.Source {
|
||||
variables["fork"] = githubv4.Boolean(false)
|
||||
} else {
|
||||
variables["fork"] = (*githubv4.Boolean)(nil)
|
||||
}
|
||||
|
||||
inputs := []string{"$perPage:Int!", "$endCursor:String", "$privacy:RepositoryPrivacy", "$fork:Boolean"}
|
||||
var ownerConnection string
|
||||
if owner == "" {
|
||||
ownerConnection = `graphql:"repositoryOwner: viewer"`
|
||||
ownerConnection = "repositoryOwner: viewer"
|
||||
} else {
|
||||
ownerConnection = `graphql:"repositoryOwner(login: $owner)"`
|
||||
ownerConnection = "repositoryOwner(login: $owner)"
|
||||
variables["owner"] = githubv4.String(owner)
|
||||
inputs = append(inputs, "$owner:String!")
|
||||
}
|
||||
|
||||
type repositoryOwner struct {
|
||||
Login string
|
||||
Repositories struct {
|
||||
Nodes []Repository
|
||||
TotalCount int
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
type result struct {
|
||||
RepositoryOwner struct {
|
||||
Login string
|
||||
Repositories struct {
|
||||
Nodes []api.Repository
|
||||
TotalCount int
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
}
|
||||
}
|
||||
} `graphql:"repositories(first: $perPage, after: $endCursor, privacy: $privacy, isFork: $fork, ownerAffiliations: OWNER, orderBy: { field: PUSHED_AT, direction: DESC })"`
|
||||
}
|
||||
}
|
||||
query := reflect.StructOf([]reflect.StructField{
|
||||
{
|
||||
Name: "RepositoryOwner",
|
||||
Type: reflect.TypeOf(repositoryOwner{}),
|
||||
Tag: reflect.StructTag(ownerConnection),
|
||||
},
|
||||
})
|
||||
|
||||
gql := graphql.NewClient(ghinstance.GraphQLEndpoint(hostname), client)
|
||||
query := fmt.Sprintf(`query RepositoryList(%s) {
|
||||
%s {
|
||||
login
|
||||
repositories(first: $perPage, after: $endCursor, privacy: $privacy, isFork: $fork, ownerAffiliations: OWNER, orderBy: { field: PUSHED_AT, direction: DESC }) {
|
||||
nodes{%s}
|
||||
totalCount
|
||||
pageInfo{hasNextPage,endCursor}
|
||||
}
|
||||
}
|
||||
}`, strings.Join(inputs, ","), ownerConnection, api.RepositoryGraphQL(filter.Fields))
|
||||
|
||||
apiClient := api.NewClientFromHTTP(client)
|
||||
listResult := RepositoryList{}
|
||||
pagination:
|
||||
for {
|
||||
result := reflect.New(query)
|
||||
err := gql.QueryNamed(context.Background(), "RepositoryList", result.Interface(), variables)
|
||||
var res result
|
||||
err := apiClient.GraphQL(hostname, query, variables, &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
owner := result.Elem().FieldByName("RepositoryOwner").Interface().(repositoryOwner)
|
||||
owner := res.RepositoryOwner
|
||||
listResult.TotalCount = owner.Repositories.TotalCount
|
||||
listResult.Owner = owner.Login
|
||||
|
||||
|
|
@ -143,47 +117,52 @@ pagination:
|
|||
}
|
||||
|
||||
func searchRepos(client *http.Client, hostname string, limit int, owner string, filter FilterOptions) (*RepositoryList, error) {
|
||||
type query struct {
|
||||
type result struct {
|
||||
Search struct {
|
||||
RepositoryCount int
|
||||
Nodes []struct {
|
||||
Repository Repository `graphql:"...on Repository"`
|
||||
}
|
||||
PageInfo struct {
|
||||
Nodes []api.Repository
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
}
|
||||
} `graphql:"search(type: REPOSITORY, query: $query, first: $perPage, after: $endCursor)"`
|
||||
}
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`query RepositoryListSearch($query:String!,$perPage:Int!,$endCursor:String) {
|
||||
search(type: REPOSITORY, query: $query, first: $perPage, after: $endCursor) {
|
||||
repositoryCount
|
||||
nodes{...on Repository{%s}}
|
||||
pageInfo{hasNextPage,endCursor}
|
||||
}
|
||||
}`, api.RepositoryGraphQL(filter.Fields))
|
||||
|
||||
perPage := limit
|
||||
if perPage > 100 {
|
||||
perPage = 100
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"query": githubv4.String(searchQuery(owner, filter)),
|
||||
"perPage": githubv4.Int(perPage),
|
||||
"endCursor": (*githubv4.String)(nil),
|
||||
"query": githubv4.String(searchQuery(owner, filter)),
|
||||
"perPage": githubv4.Int(perPage),
|
||||
}
|
||||
|
||||
gql := graphql.NewClient(ghinstance.GraphQLEndpoint(hostname), client)
|
||||
apiClient := api.NewClientFromHTTP(client)
|
||||
listResult := RepositoryList{FromSearch: true}
|
||||
pagination:
|
||||
for {
|
||||
var result query
|
||||
err := gql.QueryNamed(context.Background(), "RepositoryListSearch", &result, variables)
|
||||
var result result
|
||||
err := apiClient.GraphQL(hostname, query, variables, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
listResult.TotalCount = result.Search.RepositoryCount
|
||||
for _, node := range result.Search.Nodes {
|
||||
if listResult.Owner == "" {
|
||||
idx := strings.IndexRune(node.Repository.NameWithOwner, '/')
|
||||
listResult.Owner = node.Repository.NameWithOwner[:idx]
|
||||
for _, repo := range result.Search.Nodes {
|
||||
if listResult.Owner == "" && repo.NameWithOwner != "" {
|
||||
idx := strings.IndexRune(repo.NameWithOwner, '/')
|
||||
listResult.Owner = repo.NameWithOwner[:idx]
|
||||
}
|
||||
listResult.Repositories = append(listResult.Repositories, node.Repository)
|
||||
listResult.Repositories = append(listResult.Repositories, repo)
|
||||
if len(listResult.Repositories) >= limit {
|
||||
break pagination
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ package list
|
|||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
|
|
@ -17,6 +19,7 @@ type ListOptions struct {
|
|||
HttpClient func() (*http.Client, error)
|
||||
Config func() (config.Config, error)
|
||||
IO *iostreams.IOStreams
|
||||
Exporter cmdutil.Exporter
|
||||
|
||||
Limit int
|
||||
Owner string
|
||||
|
|
@ -88,10 +91,13 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
cmd.Flags().StringVarP(&opts.Language, "language", "l", "", "Filter by primary coding language")
|
||||
cmd.Flags().BoolVar(&opts.Archived, "archived", false, "Show only archived repositories")
|
||||
cmd.Flags().BoolVar(&opts.NonArchived, "no-archived", false, "Omit archived repositories")
|
||||
cmdutil.AddJSONFlags(cmd, &opts.Exporter, api.RepositoryFields)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
var defaultFields = []string{"nameWithOwner", "description", "isPrivate", "isFork", "isArchived", "createdAt", "pushedAt"}
|
||||
|
||||
func listRun(opts *ListOptions) error {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
|
|
@ -105,6 +111,10 @@ func listRun(opts *ListOptions) error {
|
|||
Language: opts.Language,
|
||||
Archived: opts.Archived,
|
||||
NonArchived: opts.NonArchived,
|
||||
Fields: defaultFields,
|
||||
}
|
||||
if opts.Exporter != nil {
|
||||
filter.Fields = opts.Exporter.Fields()
|
||||
}
|
||||
|
||||
cfg, err := opts.Config()
|
||||
|
|
@ -127,27 +137,31 @@ func listRun(opts *ListOptions) error {
|
|||
}
|
||||
defer opts.IO.StopPager()
|
||||
|
||||
if opts.Exporter != nil {
|
||||
return opts.Exporter.Write(opts.IO.Out, listResult.Repositories, opts.IO.ColorEnabled())
|
||||
}
|
||||
|
||||
cs := opts.IO.ColorScheme()
|
||||
tp := utils.NewTablePrinter(opts.IO)
|
||||
now := opts.Now()
|
||||
|
||||
for _, repo := range listResult.Repositories {
|
||||
info := repo.Info()
|
||||
info := repoInfo(repo)
|
||||
infoColor := cs.Gray
|
||||
if repo.IsPrivate {
|
||||
infoColor = cs.Yellow
|
||||
}
|
||||
|
||||
t := repo.PushedAt
|
||||
// if listResult.FromSearch {
|
||||
// t = repo.UpdatedAt
|
||||
// }
|
||||
if repo.PushedAt == nil {
|
||||
t = &repo.CreatedAt
|
||||
}
|
||||
|
||||
tp.AddField(repo.NameWithOwner, nil, cs.Bold)
|
||||
tp.AddField(text.ReplaceExcessiveWhitespace(repo.Description), nil, nil)
|
||||
tp.AddField(info, nil, infoColor)
|
||||
if tp.IsTTY() {
|
||||
tp.AddField(utils.FuzzyAgoAbbr(now, t), nil, cs.Gray)
|
||||
tp.AddField(utils.FuzzyAgoAbbr(now, *t), nil, cs.Gray)
|
||||
} else {
|
||||
tp.AddField(t.Format(time.RFC3339), nil, nil)
|
||||
}
|
||||
|
|
@ -179,3 +193,21 @@ func listHeader(owner string, matchCount, totalMatchCount int, hasFilters bool)
|
|||
}
|
||||
return fmt.Sprintf("Showing %d of %d repositories in @%s%s", matchCount, totalMatchCount, owner, matchStr)
|
||||
}
|
||||
|
||||
func repoInfo(r api.Repository) string {
|
||||
var tags []string
|
||||
|
||||
if r.IsPrivate {
|
||||
tags = append(tags, "private")
|
||||
} else {
|
||||
tags = append(tags, "public")
|
||||
}
|
||||
if r.IsFork {
|
||||
tags = append(tags, "fork")
|
||||
}
|
||||
if r.IsArchived {
|
||||
tags = append(tags, "archived")
|
||||
}
|
||||
|
||||
return strings.Join(tags, ", ")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,25 @@ import (
|
|||
|
||||
var NotFoundError = errors.New("not found")
|
||||
|
||||
func fetchRepository(apiClient *api.Client, repo ghrepo.Interface, fields []string) (*api.Repository, error) {
|
||||
query := fmt.Sprintf(`query RepositoryInfo($owner: String!, $name: String!) {
|
||||
repository(owner: $owner, name: $name) {%s}
|
||||
}`, api.RepositoryGraphQL(fields))
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"owner": repo.RepoOwner(),
|
||||
"name": repo.RepoName(),
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Repository api.Repository
|
||||
}
|
||||
if err := apiClient.GraphQL(repo.RepoHost(), query, variables, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return api.InitRepoHostname(&result.Repository, repo.RepoHost()), nil
|
||||
}
|
||||
|
||||
type RepoReadme struct {
|
||||
Filename string
|
||||
Content string
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ type ViewOptions struct {
|
|||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Browser browser
|
||||
Exporter cmdutil.Exporter
|
||||
|
||||
RepoArg string
|
||||
Web bool
|
||||
|
|
@ -67,10 +68,13 @@ With '--branch', view a specific branch of the repository.`,
|
|||
|
||||
cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "Open a repository in the browser")
|
||||
cmd.Flags().StringVarP(&opts.Branch, "branch", "b", "", "View a specific branch of the repository")
|
||||
cmdutil.AddJSONFlags(cmd, &opts.Exporter, api.RepositoryFields)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
var defaultFields = []string{"name", "owner", "description"}
|
||||
|
||||
func viewRun(opts *ViewOptions) error {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
|
|
@ -101,11 +105,24 @@ func viewRun(opts *ViewOptions) error {
|
|||
}
|
||||
}
|
||||
|
||||
repo, err := api.GitHubRepo(apiClient, toView)
|
||||
var readme *RepoReadme
|
||||
fields := defaultFields
|
||||
if opts.Exporter != nil {
|
||||
fields = opts.Exporter.Fields()
|
||||
}
|
||||
|
||||
repo, err := fetchRepository(apiClient, toView, fields)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !opts.Web && opts.Exporter == nil {
|
||||
readme, err = RepositoryReadme(httpClient, toView, opts.Branch)
|
||||
if err != nil && !errors.Is(err, NotFoundError) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
openURL := generateBranchURL(toView, opts.Branch)
|
||||
if opts.Web {
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
|
|
@ -114,21 +131,17 @@ func viewRun(opts *ViewOptions) error {
|
|||
return opts.Browser.Browse(openURL)
|
||||
}
|
||||
|
||||
fullName := ghrepo.FullName(toView)
|
||||
|
||||
readme, err := RepositoryReadme(httpClient, toView, opts.Branch)
|
||||
if err != nil && err != NotFoundError {
|
||||
return err
|
||||
}
|
||||
|
||||
opts.IO.DetectTerminalTheme()
|
||||
|
||||
err = opts.IO.StartPager()
|
||||
if err != nil {
|
||||
if err := opts.IO.StartPager(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer opts.IO.StopPager()
|
||||
|
||||
if opts.Exporter != nil {
|
||||
return opts.Exporter.Write(opts.IO.Out, repo, opts.IO.ColorEnabled())
|
||||
}
|
||||
|
||||
fullName := ghrepo.FullName(toView)
|
||||
stdout := opts.IO.Out
|
||||
|
||||
if !opts.IO.IsStdoutTTY() {
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ package view
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/internal/run"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
|
|
@ -625,3 +627,51 @@ func Test_ViewRun_HandlesSpecialCharacters(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_viewRun_json(t *testing.T) {
|
||||
io, _, stdout, stderr := iostreams.Test()
|
||||
io.SetStdoutTTY(false)
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
reg.StubRepoInfoResponse("OWNER", "REPO", "main")
|
||||
|
||||
opts := &ViewOptions{
|
||||
IO: io,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
Exporter: &testExporter{
|
||||
fields: []string{"name", "defaultBranchRef"},
|
||||
},
|
||||
}
|
||||
|
||||
_, teardown := run.Stub()
|
||||
defer teardown(t)
|
||||
|
||||
err := viewRun(opts)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, heredoc.Doc(`
|
||||
name: REPO
|
||||
defaultBranchRef: main
|
||||
`), stdout.String())
|
||||
assert.Equal(t, "", stderr.String())
|
||||
}
|
||||
|
||||
type testExporter struct {
|
||||
fields []string
|
||||
}
|
||||
|
||||
func (e *testExporter) Fields() []string {
|
||||
return e.fields
|
||||
}
|
||||
|
||||
func (e *testExporter) Write(w io.Writer, data interface{}, colorize bool) error {
|
||||
r := data.(*api.Repository)
|
||||
fmt.Fprintf(w, "name: %s\n", r.Name)
|
||||
fmt.Fprintf(w, "defaultBranchRef: %s\n", r.DefaultBranchRef.Name)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,11 +134,10 @@ func GetAnnotations(client *api.Client, repo ghrepo.Interface, job Job) ([]Annot
|
|||
|
||||
err := client.REST(repo.RepoHost(), "GET", path, nil, &result)
|
||||
if err != nil {
|
||||
var notFound *api.NotFoundError
|
||||
if !errors.As(err, ¬Found) {
|
||||
var httpError api.HTTPError
|
||||
if errors.As(err, &httpError) && httpError.StatusCode == 404 {
|
||||
return []Annotation{}, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
|
|
@ -149,13 +148,13 @@ func TestNewCmdRun(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_magicFieldValue(t *testing.T) {
|
||||
f, err := ioutil.TempFile("", "gh-test")
|
||||
f, err := ioutil.TempFile(t.TempDir(), "gh-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fmt.Fprint(f, "file contents")
|
||||
f.Close()
|
||||
t.Cleanup(func() { os.Remove(f.Name()) })
|
||||
|
||||
io, _, _, _ := iostreams.Test()
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
|
|
@ -26,6 +27,21 @@ func AddJSONFlags(cmd *cobra.Command, exportTarget *Exporter, fields []string) {
|
|||
f.StringP("jq", "q", "", "Filter JSON output using a jq `expression`")
|
||||
f.StringP("template", "t", "", "Format JSON output using a Go template")
|
||||
|
||||
_ = cmd.RegisterFlagCompletionFunc("json", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
var results []string
|
||||
if idx := strings.IndexRune(toComplete, ','); idx >= 0 {
|
||||
toComplete = toComplete[idx+1:]
|
||||
}
|
||||
toComplete = strings.ToLower(toComplete)
|
||||
for _, f := range fields {
|
||||
if strings.HasPrefix(strings.ToLower(f), toComplete) {
|
||||
results = append(results, f)
|
||||
}
|
||||
}
|
||||
sort.Strings(results)
|
||||
return results, cobra.ShellCompDirectiveNoSpace
|
||||
})
|
||||
|
||||
oldPreRun := cmd.PreRunE
|
||||
cmd.PreRunE = func(c *cobra.Command, args []string) error {
|
||||
if oldPreRun != nil {
|
||||
|
|
@ -102,11 +118,14 @@ func (e *exportFormat) Fields() []string {
|
|||
return e.fields
|
||||
}
|
||||
|
||||
// Write serializes data into JSON output written to w. If the object passed as data implements exportable,
|
||||
// or if data is a map or slice of exportable object, ExportData() will be called on each object to obtain
|
||||
// raw data for serialization.
|
||||
func (e *exportFormat) Write(w io.Writer, data interface{}, colorEnabled bool) error {
|
||||
buf := bytes.Buffer{}
|
||||
encoder := json.NewEncoder(&buf)
|
||||
encoder.SetEscapeHTML(false)
|
||||
if err := encoder.Encode(data); err != nil {
|
||||
if err := encoder.Encode(e.exportData(reflect.ValueOf(data))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -121,3 +140,44 @@ func (e *exportFormat) Write(w io.Writer, data interface{}, colorEnabled bool) e
|
|||
_, err := io.Copy(w, &buf)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *exportFormat) exportData(v reflect.Value) interface{} {
|
||||
switch v.Kind() {
|
||||
case reflect.Ptr, reflect.Interface:
|
||||
if !v.IsNil() {
|
||||
return e.exportData(v.Elem())
|
||||
}
|
||||
case reflect.Slice:
|
||||
a := make([]interface{}, v.Len())
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
a[i] = e.exportData(v.Index(i))
|
||||
}
|
||||
return a
|
||||
case reflect.Map:
|
||||
t := reflect.MapOf(v.Type().Key(), emptyInterfaceType)
|
||||
m := reflect.MakeMapWithSize(t, v.Len())
|
||||
iter := v.MapRange()
|
||||
for iter.Next() {
|
||||
ve := reflect.ValueOf(e.exportData(iter.Value()))
|
||||
m.SetMapIndex(iter.Key(), ve)
|
||||
}
|
||||
return m.Interface()
|
||||
case reflect.Struct:
|
||||
if v.CanAddr() && reflect.PtrTo(v.Type()).Implements(exportableType) {
|
||||
ve := v.Addr().Interface().(exportable)
|
||||
return ve.ExportData(e.fields)
|
||||
} else if v.Type().Implements(exportableType) {
|
||||
ve := v.Interface().(exportable)
|
||||
return ve.ExportData(e.fields)
|
||||
}
|
||||
}
|
||||
return v.Interface()
|
||||
}
|
||||
|
||||
type exportable interface {
|
||||
ExportData([]string) *map[string]interface{}
|
||||
}
|
||||
|
||||
var exportableType = reflect.TypeOf((*exportable)(nil)).Elem()
|
||||
var sliceOfEmptyInterface []interface{}
|
||||
var emptyInterfaceType = reflect.TypeOf(sliceOfEmptyInterface).Elem()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package cmdutil
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
|
|
@ -137,6 +138,29 @@ func Test_exportFormat_Write(t *testing.T) {
|
|||
wantW: "{\"name\":\"hubot\"}\n",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "call ExportData",
|
||||
exporter: exportFormat{fields: []string{"field1", "field2"}},
|
||||
args: args{
|
||||
data: &exportableItem{"item1"},
|
||||
colorEnabled: false,
|
||||
},
|
||||
wantW: "{\"field1\":\"item1:field1\",\"field2\":\"item1:field2\"}\n",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "recursively call ExportData",
|
||||
exporter: exportFormat{fields: []string{"f1", "f2"}},
|
||||
args: args{
|
||||
data: map[string]interface{}{
|
||||
"s1": []exportableItem{{"i1"}, {"i2"}},
|
||||
"s2": []exportableItem{{"i3"}},
|
||||
},
|
||||
colorEnabled: false,
|
||||
},
|
||||
wantW: "{\"s1\":[{\"f1\":\"i1:f1\",\"f2\":\"i1:f2\"},{\"f1\":\"i2:f1\",\"f2\":\"i2:f2\"}],\"s2\":[{\"f1\":\"i3:f1\",\"f2\":\"i3:f2\"}]}\n",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "with jq filter",
|
||||
exporter: exportFormat{filter: ".name"},
|
||||
|
|
@ -166,8 +190,20 @@ func Test_exportFormat_Write(t *testing.T) {
|
|||
return
|
||||
}
|
||||
if gotW := w.String(); gotW != tt.wantW {
|
||||
t.Errorf("exportFormat.Write() = %v, want %v", gotW, tt.wantW)
|
||||
t.Errorf("exportFormat.Write() = %q, want %q", gotW, tt.wantW)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type exportableItem struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (e *exportableItem) ExportData(fields []string) *map[string]interface{} {
|
||||
m := map[string]interface{}{}
|
||||
for _, f := range fields {
|
||||
m[f] = fmt.Sprintf("%s:%s", e.Name, f)
|
||||
}
|
||||
return &m
|
||||
}
|
||||
|
|
|
|||
|
|
@ -261,12 +261,11 @@ func TestFindLegacy(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExtractName(t *testing.T) {
|
||||
tmpfile, err := ioutil.TempFile("", "gh-cli")
|
||||
tmpfile, err := ioutil.TempFile(t.TempDir(), "gh-cli")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tmpfile.Close()
|
||||
defer os.Remove(tmpfile.Name())
|
||||
defer tmpfile.Close()
|
||||
|
||||
type args struct {
|
||||
filePath string
|
||||
|
|
@ -322,12 +321,11 @@ about: This is how you report bugs
|
|||
}
|
||||
|
||||
func TestExtractContents(t *testing.T) {
|
||||
tmpfile, err := ioutil.TempFile("", "gh-cli")
|
||||
tmpfile, err := ioutil.TempFile(t.TempDir(), "gh-cli")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tmpfile.Close()
|
||||
defer os.Remove(tmpfile.Name())
|
||||
defer tmpfile.Close()
|
||||
|
||||
type args struct {
|
||||
filePath string
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import (
|
|||
const (
|
||||
colorDelim = "1;38" // bright white
|
||||
colorKey = "1;34" // bright blue
|
||||
colorNull = "1;30" // gray
|
||||
colorNull = "36" // cyan
|
||||
colorString = "32" // green
|
||||
colorBool = "33" // yellow
|
||||
)
|
||||
|
|
|
|||
|
|
@ -142,3 +142,29 @@ Components: main
|
|||
Description: The GitHub CLI - ubuntu cosmic repo
|
||||
SignWith: C99B11DEB97541F0
|
||||
DebOverride: override.ubuntu
|
||||
|
||||
Origin: gh
|
||||
Label: gh
|
||||
Codename: hirsute
|
||||
Architectures: i386 amd64 armhf arm64
|
||||
Components: main
|
||||
Description: The GitHub CLI - ubuntu hirsute repo
|
||||
SignWith: C99B11DEB97541F0
|
||||
DebOverride: override.ubuntu
|
||||
|
||||
Origin: gh
|
||||
Label: gh
|
||||
Codename: kali-rolling
|
||||
Architectures: i386 amd64 armhf arm64
|
||||
Components: main
|
||||
Description: The GitHub CLI - kali repo
|
||||
SignWith: C99B11DEB97541F0
|
||||
|
||||
Origin: gh
|
||||
Label: gh
|
||||
Codename: impish
|
||||
Architectures: i386 amd64 armhf arm64
|
||||
Components: main
|
||||
Description: The GitHub CLI - ubuntu impish repo
|
||||
SignWith: C99B11DEB97541F0
|
||||
DebOverride: override.ubuntu
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue