Merge branch 'trunk' into feature-macos-pkg-installer
This commit is contained in:
commit
53c83fdd82
159 changed files with 7230 additions and 3250 deletions
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
|
|
@ -23,7 +23,7 @@ Please avoid:
|
|||
## Building the project
|
||||
|
||||
Prerequisites:
|
||||
- Go 1.16+
|
||||
- Go 1.21+
|
||||
|
||||
Build with:
|
||||
* Unix-like systems: `make`
|
||||
|
|
|
|||
9
.github/workflows/deployment.yml
vendored
9
.github/workflows/deployment.yml
vendored
|
|
@ -1,6 +1,6 @@
|
|||
name: Deployment
|
||||
|
||||
concurrency:
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ on:
|
|||
default: production
|
||||
type: environment
|
||||
go_version:
|
||||
default: "1.19"
|
||||
default: "1.21"
|
||||
type: string
|
||||
platforms:
|
||||
default: "linux,macos,windows"
|
||||
|
|
@ -61,7 +61,7 @@ jobs:
|
|||
dist/*.tar.gz
|
||||
dist/*.rpm
|
||||
dist/*.deb
|
||||
|
||||
|
||||
macos:
|
||||
runs-on: macos-latest
|
||||
environment: ${{ inputs.environment }}
|
||||
|
|
@ -132,7 +132,7 @@ jobs:
|
|||
dist/*.tar.gz
|
||||
dist/*.zip
|
||||
dist/*.pkg
|
||||
|
||||
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
environment: ${{ inputs.environment }}
|
||||
|
|
@ -354,6 +354,7 @@ jobs:
|
|||
if: inputs.environment == 'production' && !contains(inputs.tag_name, '-')
|
||||
with:
|
||||
formula-name: gh
|
||||
formula-path: Formula/g/gh.rb
|
||||
tag-name: ${{ inputs.tag_name }}
|
||||
push-to: cli/homebrew-core
|
||||
env:
|
||||
|
|
|
|||
4
.github/workflows/go.yml
vendored
4
.github/workflows/go.yml
vendored
|
|
@ -13,10 +13,10 @@ jobs:
|
|||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Set up Go 1.19
|
||||
- name: Set up Go 1.21
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.19
|
||||
go-version: 1.21
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
|
|
|
|||
6
.github/workflows/lint.yml
vendored
6
.github/workflows/lint.yml
vendored
|
|
@ -19,10 +19,10 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Set up Go 1.19
|
||||
- name: Set up Go 1.21
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.19
|
||||
go-version: 1.21
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
|
|
@ -40,7 +40,7 @@ jobs:
|
|||
go mod verify
|
||||
go mod download
|
||||
|
||||
LINT_VERSION=1.50.1
|
||||
LINT_VERSION=1.54.1
|
||||
curl -fsSL https://github.com/golangci/golangci-lint/releases/download/v${LINT_VERSION}/golangci-lint-${LINT_VERSION}-linux-amd64.tar.gz | \
|
||||
tar xz --strip-components 1 --wildcards \*/golangci-lint
|
||||
mkdir -p bin && mv golangci-lint bin/
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ type HTTPClientOptions struct {
|
|||
EnableCache bool
|
||||
Log io.Writer
|
||||
LogColorize bool
|
||||
LogVerboseHTTP bool
|
||||
SkipAcceptHeaders bool
|
||||
}
|
||||
|
||||
|
|
@ -35,10 +36,15 @@ func NewHTTPClient(opts HTTPClientOptions) (*http.Client, error) {
|
|||
LogIgnoreEnv: true,
|
||||
}
|
||||
|
||||
if debugEnabled, debugValue := utils.IsDebugEnabled(); debugEnabled {
|
||||
debugEnabled, debugValue := utils.IsDebugEnabled()
|
||||
if strings.Contains(debugValue, "api") {
|
||||
opts.LogVerboseHTTP = true
|
||||
}
|
||||
|
||||
if opts.LogVerboseHTTP || debugEnabled {
|
||||
clientOpts.Log = opts.Log
|
||||
clientOpts.LogColorize = opts.LogColorize
|
||||
clientOpts.LogVerboseHTTP = strings.Contains(debugValue, "api")
|
||||
clientOpts.LogVerboseHTTP = opts.LogVerboseHTTP
|
||||
}
|
||||
|
||||
headers := map[string]string{
|
||||
|
|
@ -63,8 +69,6 @@ func NewHTTPClient(opts HTTPClientOptions) (*http.Client, error) {
|
|||
client.Transport = AddAuthTokenHeader(client.Transport, opts.Config)
|
||||
}
|
||||
|
||||
client.Transport = AddASCIISanitizer(client.Transport)
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
|
|
@ -91,9 +95,17 @@ func AddAuthTokenHeader(rt http.RoundTripper, cfg tokenGetter) http.RoundTripper
|
|||
return &funcTripper{roundTrip: func(req *http.Request) (*http.Response, error) {
|
||||
// If the header is already set in the request, don't overwrite it.
|
||||
if req.Header.Get(authorization) == "" {
|
||||
hostname := ghinstance.NormalizeHostname(getHost(req))
|
||||
if token, _ := cfg.Token(hostname); token != "" {
|
||||
req.Header.Set(authorization, fmt.Sprintf("token %s", token))
|
||||
var redirectHostnameChange bool
|
||||
if req.Response != nil && req.Response.Request != nil {
|
||||
redirectHostnameChange = getHost(req) != getHost(req.Response.Request)
|
||||
}
|
||||
// Only set header if an initial request or redirect request to the same host as the initial request.
|
||||
// If the host has changed during a redirect do not add the authentication token header.
|
||||
if !redirectHostnameChange {
|
||||
hostname := ghinstance.NormalizeHostname(getHost(req))
|
||||
if token, _ := cfg.Token(hostname); token != "" {
|
||||
req.Header.Set(authorization, fmt.Sprintf("token %s", token))
|
||||
}
|
||||
}
|
||||
}
|
||||
return rt.RoundTrip(req)
|
||||
|
|
@ -128,5 +140,5 @@ func getHost(r *http.Request) string {
|
|||
if r.Host != "" {
|
||||
return r.Host
|
||||
}
|
||||
return r.URL.Hostname()
|
||||
return r.URL.Host
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
|
|
@ -16,16 +18,14 @@ import (
|
|||
|
||||
func TestNewHTTPClient(t *testing.T) {
|
||||
type args struct {
|
||||
config tokenGetter
|
||||
appVersion string
|
||||
setAccept bool
|
||||
config tokenGetter
|
||||
appVersion string
|
||||
setAccept bool
|
||||
logVerboseHTTP bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
envDebug string
|
||||
setGhDebug bool
|
||||
envGhDebug string
|
||||
host string
|
||||
wantHeader map[string]string
|
||||
wantStderr string
|
||||
|
|
@ -33,9 +33,10 @@ func TestNewHTTPClient(t *testing.T) {
|
|||
{
|
||||
name: "github.com with Accept header",
|
||||
args: args{
|
||||
config: tinyConfig{"github.com:oauth_token": "MYTOKEN"},
|
||||
appVersion: "v1.2.3",
|
||||
setAccept: true,
|
||||
config: tinyConfig{"github.com:oauth_token": "MYTOKEN"},
|
||||
appVersion: "v1.2.3",
|
||||
setAccept: true,
|
||||
logVerboseHTTP: false,
|
||||
},
|
||||
host: "github.com",
|
||||
wantHeader: map[string]string{
|
||||
|
|
@ -48,9 +49,10 @@ func TestNewHTTPClient(t *testing.T) {
|
|||
{
|
||||
name: "github.com no Accept header",
|
||||
args: args{
|
||||
config: tinyConfig{"github.com:oauth_token": "MYTOKEN"},
|
||||
appVersion: "v1.2.3",
|
||||
setAccept: false,
|
||||
config: tinyConfig{"github.com:oauth_token": "MYTOKEN"},
|
||||
appVersion: "v1.2.3",
|
||||
setAccept: false,
|
||||
logVerboseHTTP: false,
|
||||
},
|
||||
host: "github.com",
|
||||
wantHeader: map[string]string{
|
||||
|
|
@ -63,9 +65,10 @@ func TestNewHTTPClient(t *testing.T) {
|
|||
{
|
||||
name: "github.com no authentication token",
|
||||
args: args{
|
||||
config: tinyConfig{"example.com:oauth_token": "MYTOKEN"},
|
||||
appVersion: "v1.2.3",
|
||||
setAccept: true,
|
||||
config: tinyConfig{"example.com:oauth_token": "MYTOKEN"},
|
||||
appVersion: "v1.2.3",
|
||||
setAccept: true,
|
||||
logVerboseHTTP: false,
|
||||
},
|
||||
host: "github.com",
|
||||
wantHeader: map[string]string{
|
||||
|
|
@ -78,45 +81,12 @@ func TestNewHTTPClient(t *testing.T) {
|
|||
{
|
||||
name: "github.com in verbose mode",
|
||||
args: args{
|
||||
config: tinyConfig{"github.com:oauth_token": "MYTOKEN"},
|
||||
appVersion: "v1.2.3",
|
||||
setAccept: true,
|
||||
config: tinyConfig{"github.com:oauth_token": "MYTOKEN"},
|
||||
appVersion: "v1.2.3",
|
||||
setAccept: true,
|
||||
logVerboseHTTP: true,
|
||||
},
|
||||
host: "github.com",
|
||||
envDebug: "api",
|
||||
setGhDebug: false,
|
||||
wantHeader: map[string]string{
|
||||
"authorization": "token MYTOKEN",
|
||||
"user-agent": "GitHub CLI v1.2.3",
|
||||
"accept": "application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview",
|
||||
},
|
||||
wantStderr: heredoc.Doc(`
|
||||
* Request at <time>
|
||||
* Request to http://<host>:<port>
|
||||
> GET / HTTP/1.1
|
||||
> Host: github.com
|
||||
> Accept: application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview
|
||||
> Authorization: token ████████████████████
|
||||
> Content-Type: application/json; charset=utf-8
|
||||
> Time-Zone: <timezone>
|
||||
> User-Agent: GitHub CLI v1.2.3
|
||||
|
||||
< HTTP/1.1 204 No Content
|
||||
< Date: <time>
|
||||
|
||||
* Request took <duration>
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "github.com in verbose mode",
|
||||
args: args{
|
||||
config: tinyConfig{"github.com:oauth_token": "MYTOKEN"},
|
||||
appVersion: "v1.2.3",
|
||||
setAccept: true,
|
||||
},
|
||||
host: "github.com",
|
||||
envGhDebug: "api",
|
||||
setGhDebug: true,
|
||||
host: "github.com",
|
||||
wantHeader: map[string]string{
|
||||
"authorization": "token MYTOKEN",
|
||||
"user-agent": "GitHub CLI v1.2.3",
|
||||
|
|
@ -165,19 +135,13 @@ func TestNewHTTPClient(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Setenv("DEBUG", tt.envDebug)
|
||||
if tt.setGhDebug {
|
||||
t.Setenv("GH_DEBUG", tt.envGhDebug)
|
||||
} else {
|
||||
os.Unsetenv("GH_DEBUG")
|
||||
}
|
||||
|
||||
ios, _, _, stderr := iostreams.Test()
|
||||
client, err := NewHTTPClient(HTTPClientOptions{
|
||||
AppVersion: tt.args.appVersion,
|
||||
Config: tt.args.config,
|
||||
Log: ios.ErrOut,
|
||||
SkipAcceptHeaders: !tt.args.setAccept,
|
||||
LogVerboseHTTP: tt.args.logVerboseHTTP,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -200,6 +164,112 @@ func TestNewHTTPClient(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestHTTPClientRedirectAuthenticationHeaderHandling(t *testing.T) {
|
||||
var request *http.Request
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
request = r
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
var redirectRequest *http.Request
|
||||
redirectServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
redirectRequest = r
|
||||
http.Redirect(w, r, server.URL, http.StatusFound)
|
||||
}))
|
||||
defer redirectServer.Close()
|
||||
|
||||
client, err := NewHTTPClient(HTTPClientOptions{
|
||||
Config: tinyConfig{
|
||||
fmt.Sprintf("%s:oauth_token", strings.TrimPrefix(redirectServer.URL, "http://")): "REDIRECT-TOKEN",
|
||||
fmt.Sprintf("%s:oauth_token", strings.TrimPrefix(server.URL, "http://")): "TOKEN",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest("GET", redirectServer.URL, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "token REDIRECT-TOKEN", redirectRequest.Header.Get(authorization))
|
||||
assert.Equal(t, "", request.Header.Get(authorization))
|
||||
assert.Equal(t, 204, res.StatusCode)
|
||||
}
|
||||
|
||||
func TestHTTPClientSanitizeJSONControlCharactersC0(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
issue := Issue{
|
||||
Title: "\u001B[31mRed Title\u001B[0m",
|
||||
Body: "1\u0001 2\u0002 3\u0003 4\u0004 5\u0005 6\u0006 7\u0007 8\u0008 9\t A\r\n B\u000b C\u000c D\r\n E\u000e F\u000f",
|
||||
Author: Author{
|
||||
ID: "1",
|
||||
Name: "10\u0010 11\u0011 12\u0012 13\u0013 14\u0014 15\u0015 16\u0016 17\u0017 18\u0018 19\u0019 1A\u001a 1B\u001b 1C\u001c 1D\u001d 1E\u001e 1F\u001f",
|
||||
Login: "monalisa \\u00\u001b",
|
||||
},
|
||||
ActiveLockReason: "Escaped \u001B \\u001B \\\u001B \\\\u001B",
|
||||
}
|
||||
responseData, _ := json.Marshal(issue)
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
fmt.Fprint(w, string(responseData))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client, err := NewHTTPClient(HTTPClientOptions{})
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequest("GET", ts.URL, nil)
|
||||
require.NoError(t, err)
|
||||
res, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
require.NoError(t, err)
|
||||
var issue Issue
|
||||
err = json.Unmarshal(body, &issue)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "^[[31mRed Title^[[0m", issue.Title)
|
||||
assert.Equal(t, "1^A 2^B 3^C 4^D 5^E 6^F 7^G 8^H 9\t A\r\n B\v C^L D\r\n E^N F^O", issue.Body)
|
||||
assert.Equal(t, "10^P 11^Q 12^R 13^S 14^T 15^U 16^V 17^W 18^X 19^Y 1A^Z 1B^[ 1C^\\ 1D^] 1E^^ 1F^_", issue.Author.Name)
|
||||
assert.Equal(t, "monalisa \\u00^[", issue.Author.Login)
|
||||
assert.Equal(t, "Escaped ^[ \\^[ \\^[ \\\\^[", issue.ActiveLockReason)
|
||||
}
|
||||
|
||||
func TestHTTPClientSanitizeControlCharactersC1(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
issue := Issue{
|
||||
Title: "\xC2\x9B[31mRed Title\xC2\x9B[0m",
|
||||
Body: "80\xC2\x80 81\xC2\x81 82\xC2\x82 83\xC2\x83 84\xC2\x84 85\xC2\x85 86\xC2\x86 87\xC2\x87 88\xC2\x88 89\xC2\x89 8A\xC2\x8A 8B\xC2\x8B 8C\xC2\x8C 8D\xC2\x8D 8E\xC2\x8E 8F\xC2\x8F",
|
||||
Author: Author{
|
||||
ID: "1",
|
||||
Name: "90\xC2\x90 91\xC2\x91 92\xC2\x92 93\xC2\x93 94\xC2\x94 95\xC2\x95 96\xC2\x96 97\xC2\x97 98\xC2\x98 99\xC2\x99 9A\xC2\x9A 9B\xC2\x9B 9C\xC2\x9C 9D\xC2\x9D 9E\xC2\x9E 9F\xC2\x9F",
|
||||
Login: "monalisa\xC2\xA1",
|
||||
},
|
||||
}
|
||||
responseData, _ := json.Marshal(issue)
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
fmt.Fprint(w, string(responseData))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client, err := NewHTTPClient(HTTPClientOptions{})
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequest("GET", ts.URL, nil)
|
||||
require.NoError(t, err)
|
||||
res, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
require.NoError(t, err)
|
||||
var issue Issue
|
||||
err = json.Unmarshal(body, &issue)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "^[[31mRed Title^[[0m", issue.Title)
|
||||
assert.Equal(t, "80^@ 81^A 82^B 83^C 84^D 85^E 86^F 87^G 88^H 89^I 8A^J 8B^K 8C^L 8D^M 8E^N 8F^O", issue.Body)
|
||||
assert.Equal(t, "90^P 91^Q 92^R 93^S 94^T 95^U 96^V 97^W 98^X 99^Y 9A^Z 9B^[ 9C^\\ 9D^] 9E^^ 9F^_", issue.Author.Name)
|
||||
assert.Equal(t, "monalisa¡", issue.Author.Login)
|
||||
}
|
||||
|
||||
type tinyConfig map[string]string
|
||||
|
||||
func (c tinyConfig) Token(host string) (string, string) {
|
||||
|
|
|
|||
|
|
@ -4,63 +4,16 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/shurcooL/githubv4"
|
||||
)
|
||||
|
||||
type LinkedBranch struct {
|
||||
ID string
|
||||
BranchName string
|
||||
RepoUrl string
|
||||
URL string
|
||||
}
|
||||
|
||||
// method to return url of linked branch, adds the branch name to the end of the repo url
|
||||
func (b *LinkedBranch) Url() string {
|
||||
return fmt.Sprintf("%s/tree/%s", b.RepoUrl, b.BranchName)
|
||||
}
|
||||
|
||||
func nameParam(params map[string]interface{}) string {
|
||||
if params["name"] != "" {
|
||||
return "name: $name,"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func nameArg(params map[string]interface{}) string {
|
||||
if params["name"] != "" {
|
||||
return "$name: String, "
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func CreateBranchIssueReference(client *Client, repo *Repository, params map[string]interface{}) (*LinkedBranch, error) {
|
||||
query := fmt.Sprintf(`
|
||||
mutation CreateLinkedBranch($issueId: ID!, $oid: GitObjectID!, %[1]s$repositoryId: ID) {
|
||||
createLinkedBranch(input: {
|
||||
issueId: $issueId,
|
||||
%[2]s
|
||||
oid: $oid,
|
||||
repositoryId: $repositoryId
|
||||
}) {
|
||||
linkedBranch {
|
||||
id
|
||||
ref {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}`, nameArg(params), nameParam(params))
|
||||
|
||||
inputParams := map[string]interface{}{
|
||||
"repositoryId": repo.ID,
|
||||
}
|
||||
|
||||
for key, val := range params {
|
||||
switch key {
|
||||
case "issueId", "name", "oid":
|
||||
inputParams[key] = val
|
||||
}
|
||||
}
|
||||
|
||||
result := struct {
|
||||
func CreateLinkedBranch(client *Client, host string, repoID, issueID, branchID, branchName string) (string, error) {
|
||||
var mutation struct {
|
||||
CreateLinkedBranch struct {
|
||||
LinkedBranch struct {
|
||||
ID string
|
||||
|
|
@ -68,90 +21,76 @@ func CreateBranchIssueReference(client *Client, repo *Repository, params map[str
|
|||
Name string
|
||||
}
|
||||
}
|
||||
}
|
||||
}{}
|
||||
|
||||
if err := client.GraphQL(repo.RepoHost(), query, inputParams, &result); err != nil {
|
||||
return nil, err
|
||||
} `graphql:"createLinkedBranch(input: $input)"`
|
||||
}
|
||||
|
||||
ref := LinkedBranch{
|
||||
ID: result.CreateLinkedBranch.LinkedBranch.ID,
|
||||
BranchName: result.CreateLinkedBranch.LinkedBranch.Ref.Name,
|
||||
input := githubv4.CreateLinkedBranchInput{
|
||||
IssueID: githubv4.ID(issueID),
|
||||
Oid: githubv4.GitObjectID(branchID),
|
||||
}
|
||||
if repoID != "" {
|
||||
repo := githubv4.ID(repoID)
|
||||
input.RepositoryID = &repo
|
||||
}
|
||||
if branchName != "" {
|
||||
name := githubv4.String(branchName)
|
||||
input.Name = &name
|
||||
}
|
||||
variables := map[string]interface{}{
|
||||
"input": input,
|
||||
}
|
||||
|
||||
return &ref, nil
|
||||
err := client.Mutate(host, "CreateLinkedBranch", &mutation, variables)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return mutation.CreateLinkedBranch.LinkedBranch.Ref.Name, nil
|
||||
}
|
||||
|
||||
func ListLinkedBranches(client *Client, repo ghrepo.Interface, issueNumber int) ([]LinkedBranch, error) {
|
||||
query := `
|
||||
query BranchIssueReferenceListLinkedBranches($repositoryName: String!, $repositoryOwner: String!, $issueNumber: Int!) {
|
||||
repository(name: $repositoryName, owner: $repositoryOwner) {
|
||||
issue(number: $issueNumber) {
|
||||
linkedBranches(first: 30) {
|
||||
edges {
|
||||
node {
|
||||
ref {
|
||||
name
|
||||
repository {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"repositoryName": repo.RepoName(),
|
||||
"repositoryOwner": repo.RepoOwner(),
|
||||
"issueNumber": issueNumber,
|
||||
}
|
||||
|
||||
result := struct {
|
||||
var query struct {
|
||||
Repository struct {
|
||||
Issue struct {
|
||||
LinkedBranches struct {
|
||||
Edges []struct {
|
||||
Node struct {
|
||||
Ref struct {
|
||||
Name string
|
||||
Repository struct {
|
||||
NameWithOwner string
|
||||
Url string
|
||||
}
|
||||
Nodes []struct {
|
||||
Ref struct {
|
||||
Name string
|
||||
Repository struct {
|
||||
Url string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}{}
|
||||
} `graphql:"linkedBranches(first: 30)"`
|
||||
} `graphql:"issue(number: $number)"`
|
||||
} `graphql:"repository(owner: $owner, name: $name)"`
|
||||
}
|
||||
|
||||
if err := client.GraphQL(repo.RepoHost(), query, variables, &result); err != nil {
|
||||
variables := map[string]interface{}{
|
||||
"number": githubv4.Int(issueNumber),
|
||||
"owner": githubv4.String(repo.RepoOwner()),
|
||||
"name": githubv4.String(repo.RepoName()),
|
||||
}
|
||||
|
||||
if err := client.Query(repo.RepoHost(), "ListLinkedBranches", &query, variables); err != nil {
|
||||
return []LinkedBranch{}, err
|
||||
}
|
||||
|
||||
var branchNames []LinkedBranch
|
||||
|
||||
for _, edge := range result.Repository.Issue.LinkedBranches.Edges {
|
||||
for _, node := range query.Repository.Issue.LinkedBranches.Nodes {
|
||||
branch := LinkedBranch{
|
||||
BranchName: edge.Node.Ref.Name,
|
||||
RepoUrl: edge.Node.Ref.Repository.Url,
|
||||
BranchName: node.Ref.Name,
|
||||
URL: fmt.Sprintf("%s/tree/%s", node.Ref.Repository.Url, node.Ref.Name),
|
||||
}
|
||||
|
||||
branchNames = append(branchNames, branch)
|
||||
}
|
||||
|
||||
return branchNames, nil
|
||||
}
|
||||
|
||||
// introspects the schema to see if we expose the LinkedBranch type
|
||||
func CheckLinkedBranchFeature(client *Client, host string) (err error) {
|
||||
var featureDetection struct {
|
||||
func CheckLinkedBranchFeature(client *Client, host string) error {
|
||||
var query struct {
|
||||
Name struct {
|
||||
Fields []struct {
|
||||
Name string
|
||||
|
|
@ -159,43 +98,21 @@ func CheckLinkedBranchFeature(client *Client, host string) (err error) {
|
|||
} `graphql:"LinkedBranch: __type(name: \"LinkedBranch\")"`
|
||||
}
|
||||
|
||||
if err := client.Query(host, "LinkedBranch_fields", &featureDetection, nil); err != nil {
|
||||
if err := client.Query(host, "LinkedBranchFeature", &query, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(featureDetection.Name.Fields) == 0 {
|
||||
if len(query.Name.Fields) == 0 {
|
||||
return fmt.Errorf("the `gh issue develop` command is not currently available")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// This fetches the oids for the repo's default branch (`main`, etc) and the name the user might have provided in one shot.
|
||||
func FindBaseOid(client *Client, repo *Repository, ref string) (string, string, error) {
|
||||
query := `
|
||||
query BranchIssueReferenceFindBaseOid($repositoryName: String!, $repositoryOwner: String!, $ref: String!) {
|
||||
repository(name: $repositoryName, owner: $repositoryOwner) {
|
||||
defaultBranchRef {
|
||||
target {
|
||||
oid
|
||||
}
|
||||
}
|
||||
ref(qualifiedName: $ref) {
|
||||
target {
|
||||
oid
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"repositoryName": repo.Name,
|
||||
"repositoryOwner": repo.RepoOwner(),
|
||||
"ref": ref,
|
||||
}
|
||||
|
||||
result := struct {
|
||||
func FindRepoBranchID(client *Client, repo ghrepo.Interface, ref string) (string, string, error) {
|
||||
var query struct {
|
||||
Repository struct {
|
||||
Id string
|
||||
DefaultBranchRef struct {
|
||||
Target struct {
|
||||
Oid string
|
||||
|
|
@ -205,13 +122,24 @@ func FindBaseOid(client *Client, repo *Repository, ref string) (string, string,
|
|||
Target struct {
|
||||
Oid string
|
||||
}
|
||||
}
|
||||
}
|
||||
}{}
|
||||
} `graphql:"ref(qualifiedName: $ref)"`
|
||||
} `graphql:"repository(owner: $owner, name: $name)"`
|
||||
}
|
||||
|
||||
if err := client.GraphQL(repo.RepoHost(), query, variables, &result); err != nil {
|
||||
variables := map[string]interface{}{
|
||||
"ref": githubv4.String(ref),
|
||||
"owner": githubv4.String(repo.RepoOwner()),
|
||||
"name": githubv4.String(repo.RepoName()),
|
||||
}
|
||||
|
||||
if err := client.Query(repo.RepoHost(), "FindRepoBranchID", &query, variables); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return result.Repository.Ref.Target.Oid, result.Repository.DefaultBranchRef.Target.Oid, nil
|
||||
branchID := query.Repository.Ref.Target.Oid
|
||||
if branchID == "" {
|
||||
branchID = query.Repository.DefaultBranchRef.Target.Oid
|
||||
}
|
||||
|
||||
return query.Repository.Id, branchID, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1370,3 +1370,21 @@ func GetRepoIDs(client *Client, host string, repositories []ghrepo.Interface) ([
|
|||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func RepoExists(client *Client, repo ghrepo.Interface) (bool, error) {
|
||||
path := fmt.Sprintf("%srepos/%s/%s", ghinstance.RESTPrefix(repo.RepoHost()), repo.RepoOwner(), repo.RepoName())
|
||||
|
||||
resp, err := client.HTTP().Head(path)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
switch resp.StatusCode {
|
||||
case 200:
|
||||
return true, nil
|
||||
case 404:
|
||||
return false, nil
|
||||
default:
|
||||
return false, ghAPI.HandleHTTPError(resp)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGitHubRepo_notFound(t *testing.T) {
|
||||
|
|
@ -467,3 +468,72 @@ func TestDisplayName(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoExists(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
httpStub func(*httpmock.Registry)
|
||||
repo ghrepo.Interface
|
||||
existCheck bool
|
||||
wantErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "repo exists",
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
httpmock.REST("HEAD", "repos/OWNER/REPO"),
|
||||
httpmock.StringResponse("{}"),
|
||||
)
|
||||
},
|
||||
repo: ghrepo.New("OWNER", "REPO"),
|
||||
existCheck: true,
|
||||
wantErrMsg: "",
|
||||
},
|
||||
{
|
||||
name: "repo does not exists",
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
httpmock.REST("HEAD", "repos/OWNER/REPO"),
|
||||
httpmock.StatusStringResponse(404, "Not Found"),
|
||||
)
|
||||
},
|
||||
repo: ghrepo.New("OWNER", "REPO"),
|
||||
existCheck: false,
|
||||
wantErrMsg: "",
|
||||
},
|
||||
{
|
||||
name: "http error",
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
httpmock.REST("HEAD", "repos/OWNER/REPO"),
|
||||
httpmock.StatusStringResponse(500, "Internal Server Error"),
|
||||
)
|
||||
},
|
||||
repo: ghrepo.New("OWNER", "REPO"),
|
||||
existCheck: false,
|
||||
wantErrMsg: "HTTP 500 (https://api.github.com/repos/OWNER/REPO)",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
reg := &httpmock.Registry{}
|
||||
if tt.httpStub != nil {
|
||||
tt.httpStub(reg)
|
||||
}
|
||||
|
||||
client := newTestClient(reg)
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
exist, err := RepoExists(client, ghrepo.New("OWNER", "REPO"))
|
||||
if tt.wantErrMsg != "" {
|
||||
assert.Equal(t, tt.wantErrMsg, err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
if exist != tt.existCheck {
|
||||
t.Errorf("RepoExists() returns %v, expected %v", exist, tt.existCheck)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -205,11 +205,15 @@ func StatusCheckRollupGraphQLWithoutCountByState(after string) string {
|
|||
}`), afterClause)
|
||||
}
|
||||
|
||||
func RequiredStatusCheckRollupGraphQL(prID, after string) string {
|
||||
func RequiredStatusCheckRollupGraphQL(prID, after string, includeEvent bool) string {
|
||||
var afterClause string
|
||||
if after != "" {
|
||||
afterClause = ",after:" + after
|
||||
}
|
||||
eventField := "event,"
|
||||
if !includeEvent {
|
||||
eventField = ""
|
||||
}
|
||||
return fmt.Sprintf(shortenQuery(`
|
||||
statusCheckRollup: commits(last: 1) {
|
||||
nodes {
|
||||
|
|
@ -228,7 +232,7 @@ func RequiredStatusCheckRollupGraphQL(prID, after string) string {
|
|||
},
|
||||
...on CheckRun {
|
||||
name,
|
||||
checkSuite{workflowRun{event,workflow{name}}},
|
||||
checkSuite{workflowRun{%[3]sworkflow{name}}},
|
||||
status,
|
||||
conclusion,
|
||||
startedAt,
|
||||
|
|
@ -242,7 +246,7 @@ func RequiredStatusCheckRollupGraphQL(prID, after string) string {
|
|||
}
|
||||
}
|
||||
}
|
||||
}`), afterClause, prID)
|
||||
}`), afterClause, prID, eventField)
|
||||
}
|
||||
|
||||
var IssueFields = []string{
|
||||
|
|
|
|||
|
|
@ -1,223 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
var jsonTypeRE = regexp.MustCompile(`[/+]json($|;)`)
|
||||
|
||||
// GitHub servers do not sanitize their API output for terminal display
|
||||
// and leave in unescaped ASCII control characters.
|
||||
// C0 control characters are represented in their unicode code point form ranging from \u0000 to \u001F.
|
||||
// C1 control characters are represented in two bytes, the first being 0xC2 and the second ranging from 0x80 to 0x9F.
|
||||
// These control characters will be interpreted by the terminal, this behaviour can be
|
||||
// used maliciously as an attack vector, especially the control characters \u001B and \u009B.
|
||||
// This function wraps JSON response bodies in a ReadCloser that transforms C0 and C1
|
||||
// control characters to their caret notations respectively so that the terminal will not
|
||||
// interpret them.
|
||||
func AddASCIISanitizer(rt http.RoundTripper) http.RoundTripper {
|
||||
return &funcTripper{roundTrip: func(req *http.Request) (*http.Response, error) {
|
||||
res, err := rt.RoundTrip(req)
|
||||
if err != nil || !jsonTypeRE.MatchString(res.Header.Get("Content-Type")) {
|
||||
return res, err
|
||||
}
|
||||
res.Body = sanitizedReadCloser(res.Body)
|
||||
return res, err
|
||||
}}
|
||||
}
|
||||
|
||||
func sanitizedReadCloser(rc io.ReadCloser) io.ReadCloser {
|
||||
return struct {
|
||||
io.Reader
|
||||
io.Closer
|
||||
}{
|
||||
Reader: transform.NewReader(rc, &sanitizer{}),
|
||||
Closer: rc,
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitizer implements transform.Transformer interface.
|
||||
type sanitizer struct {
|
||||
addEscape bool
|
||||
}
|
||||
|
||||
// Transform uses a sliding window algorithm to detect C0 and C1
|
||||
// ASCII control sequences as they are read and replaces them
|
||||
// with equivalent inert characters. Characters that are not part
|
||||
// of a control sequence are not modified.
|
||||
func (t *sanitizer) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||
lSrc := len(src)
|
||||
lDst := len(dst)
|
||||
|
||||
for nSrc < lSrc-6 && nDst < lDst {
|
||||
window := src[nSrc : nSrc+6]
|
||||
|
||||
// Replace C1 Control Characters
|
||||
if repl, found := mapC1ToCaret(window[:2]); found {
|
||||
if len(repl)+nDst > lDst {
|
||||
err = transform.ErrShortDst
|
||||
return
|
||||
}
|
||||
for j := 0; j < len(repl); j++ {
|
||||
dst[nDst] = repl[j]
|
||||
nDst++
|
||||
}
|
||||
nSrc += 2
|
||||
continue
|
||||
}
|
||||
|
||||
// Replace C0 Control Characters
|
||||
if repl, found := mapC0ToCaret(window); found {
|
||||
if t.addEscape {
|
||||
repl = append([]byte{'\\'}, repl...)
|
||||
}
|
||||
if len(repl)+nDst > lDst {
|
||||
err = transform.ErrShortDst
|
||||
return
|
||||
}
|
||||
for j := 0; j < len(repl); j++ {
|
||||
dst[nDst] = repl[j]
|
||||
nDst++
|
||||
}
|
||||
t.addEscape = false
|
||||
nSrc += 6
|
||||
continue
|
||||
}
|
||||
|
||||
if window[0] == '\\' {
|
||||
t.addEscape = !t.addEscape
|
||||
} else {
|
||||
t.addEscape = false
|
||||
}
|
||||
|
||||
dst[nDst] = src[nSrc]
|
||||
nDst++
|
||||
nSrc++
|
||||
}
|
||||
|
||||
if !atEOF {
|
||||
err = transform.ErrShortSrc
|
||||
return
|
||||
}
|
||||
|
||||
remaining := lSrc - nSrc
|
||||
if remaining+nDst > lDst {
|
||||
err = transform.ErrShortDst
|
||||
return
|
||||
}
|
||||
|
||||
for j := 0; j < remaining; j++ {
|
||||
dst[nDst] = src[nSrc]
|
||||
nDst++
|
||||
nSrc++
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (t *sanitizer) Reset() {
|
||||
t.addEscape = false
|
||||
}
|
||||
|
||||
// mapC0ToCaret maps C0 control sequences to caret notation.
|
||||
func mapC0ToCaret(b []byte) ([]byte, bool) {
|
||||
if len(b) != 6 {
|
||||
return b, false
|
||||
}
|
||||
if !bytes.HasPrefix(b, []byte(`\u00`)) {
|
||||
return b, false
|
||||
}
|
||||
m := map[string]string{
|
||||
`\u0000`: `^@`,
|
||||
`\u0001`: `^A`,
|
||||
`\u0002`: `^B`,
|
||||
`\u0003`: `^C`,
|
||||
`\u0004`: `^D`,
|
||||
`\u0005`: `^E`,
|
||||
`\u0006`: `^F`,
|
||||
`\u0007`: `^G`,
|
||||
`\u0008`: `^H`,
|
||||
`\u0009`: `^I`,
|
||||
`\u000a`: `^J`,
|
||||
`\u000b`: `^K`,
|
||||
`\u000c`: `^L`,
|
||||
`\u000d`: `^M`,
|
||||
`\u000e`: `^N`,
|
||||
`\u000f`: `^O`,
|
||||
`\u0010`: `^P`,
|
||||
`\u0011`: `^Q`,
|
||||
`\u0012`: `^R`,
|
||||
`\u0013`: `^S`,
|
||||
`\u0014`: `^T`,
|
||||
`\u0015`: `^U`,
|
||||
`\u0016`: `^V`,
|
||||
`\u0017`: `^W`,
|
||||
`\u0018`: `^X`,
|
||||
`\u0019`: `^Y`,
|
||||
`\u001a`: `^Z`,
|
||||
`\u001b`: `^[`,
|
||||
`\u001c`: `^\\`,
|
||||
`\u001d`: `^]`,
|
||||
`\u001e`: `^^`,
|
||||
`\u001f`: `^_`,
|
||||
}
|
||||
if c, ok := m[strings.ToLower(string(b))]; ok {
|
||||
return []byte(c), true
|
||||
}
|
||||
return b, false
|
||||
}
|
||||
|
||||
// mapC1ToCaret maps C1 control sequences to caret notation.
|
||||
// C1 control sequences are two bytes long where the first byte is 0xC2.
|
||||
func mapC1ToCaret(b []byte) ([]byte, bool) {
|
||||
if len(b) != 2 {
|
||||
return b, false
|
||||
}
|
||||
if b[0] != 0xC2 {
|
||||
return b, false
|
||||
}
|
||||
m := map[byte]string{
|
||||
128: `^@`,
|
||||
129: `^A`,
|
||||
130: `^B`,
|
||||
131: `^C`,
|
||||
132: `^D`,
|
||||
133: `^E`,
|
||||
134: `^F`,
|
||||
135: `^G`,
|
||||
136: `^H`,
|
||||
137: `^I`,
|
||||
138: `^J`,
|
||||
139: `^K`,
|
||||
140: `^L`,
|
||||
141: `^M`,
|
||||
142: `^N`,
|
||||
143: `^O`,
|
||||
144: `^P`,
|
||||
145: `^Q`,
|
||||
146: `^R`,
|
||||
147: `^S`,
|
||||
148: `^T`,
|
||||
149: `^U`,
|
||||
150: `^V`,
|
||||
151: `^W`,
|
||||
152: `^X`,
|
||||
153: `^Y`,
|
||||
154: `^Z`,
|
||||
155: `^[`,
|
||||
156: `^\\`,
|
||||
157: `^]`,
|
||||
158: `^^`,
|
||||
159: `^_`,
|
||||
}
|
||||
if c, ok := m[b[1]]; ok {
|
||||
return []byte(c), true
|
||||
}
|
||||
return b, false
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"testing/iotest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHTTPClientSanitizeASCIIControlCharactersC0(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
issue := Issue{
|
||||
Title: "\u001B[31mRed Title\u001B[0m",
|
||||
Body: "1\u0001 2\u0002 3\u0003 4\u0004 5\u0005 6\u0006 7\u0007 8\u0008 9\t A\r\n B\u000b C\u000c D\r\n E\u000e F\u000f",
|
||||
Author: Author{
|
||||
ID: "1",
|
||||
Name: "10\u0010 11\u0011 12\u0012 13\u0013 14\u0014 15\u0015 16\u0016 17\u0017 18\u0018 19\u0019 1A\u001a 1B\u001b 1C\u001c 1D\u001d 1E\u001e 1F\u001f",
|
||||
Login: "monalisa \\u00\u001b",
|
||||
},
|
||||
ActiveLockReason: "Escaped \u001B \\u001B \\\u001B \\\\u001B",
|
||||
}
|
||||
responseData, _ := json.Marshal(issue)
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
fmt.Fprint(w, string(responseData))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client, err := NewHTTPClient(HTTPClientOptions{})
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequest("GET", ts.URL, nil)
|
||||
require.NoError(t, err)
|
||||
res, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
require.NoError(t, err)
|
||||
var issue Issue
|
||||
err = json.Unmarshal(body, &issue)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "^[[31mRed Title^[[0m", issue.Title)
|
||||
assert.Equal(t, "1^A 2^B 3^C 4^D 5^E 6^F 7^G 8^H 9\t A\r\n B^K C^L D\r\n E^N F^O", issue.Body)
|
||||
assert.Equal(t, "10^P 11^Q 12^R 13^S 14^T 15^U 16^V 17^W 18^X 19^Y 1A^Z 1B^[ 1C^\\ 1D^] 1E^^ 1F^_", issue.Author.Name)
|
||||
assert.Equal(t, "monalisa \\u00^[", issue.Author.Login)
|
||||
assert.Equal(t, "Escaped ^[ \\^[ \\^[ \\\\^[", issue.ActiveLockReason)
|
||||
}
|
||||
|
||||
func TestHTTPClientSanitizeASCIIControlCharactersC1(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
issue := Issue{
|
||||
Title: "\xC2\x9B[31mRed Title\xC2\x9B[0m",
|
||||
Body: "80\xC2\x80 81\xC2\x81 82\xC2\x82 83\xC2\x83 84\xC2\x84 85\xC2\x85 86\xC2\x86 87\xC2\x87 88\xC2\x88 89\xC2\x89 8A\xC2\x8A 8B\xC2\x8B 8C\xC2\x8C 8D\xC2\x8D 8E\xC2\x8E 8F\xC2\x8F",
|
||||
Author: Author{
|
||||
ID: "1",
|
||||
Name: "90\xC2\x90 91\xC2\x91 92\xC2\x92 93\xC2\x93 94\xC2\x94 95\xC2\x95 96\xC2\x96 97\xC2\x97 98\xC2\x98 99\xC2\x99 9A\xC2\x9A 9B\xC2\x9B 9C\xC2\x9C 9D\xC2\x9D 9E\xC2\x9E 9F\xC2\x9F",
|
||||
Login: "monalisa\xC2\xA1",
|
||||
},
|
||||
}
|
||||
responseData, _ := json.Marshal(issue)
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
fmt.Fprint(w, string(responseData))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client, err := NewHTTPClient(HTTPClientOptions{})
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequest("GET", ts.URL, nil)
|
||||
require.NoError(t, err)
|
||||
res, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
require.NoError(t, err)
|
||||
var issue Issue
|
||||
err = json.Unmarshal(body, &issue)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "^[[31mRed Title^[[0m", issue.Title)
|
||||
assert.Equal(t, "80^@ 81^A 82^B 83^C 84^D 85^E 86^F 87^G 88^H 89^I 8A^J 8B^K 8C^L 8D^M 8E^N 8F^O", issue.Body)
|
||||
assert.Equal(t, "90^P 91^Q 92^R 93^S 94^T 95^U 96^V 97^W 98^X 99^Y 9A^Z 9B^[ 9C^\\ 9D^] 9E^^ 9F^_", issue.Author.Name)
|
||||
assert.Equal(t, "monalisa¡", issue.Author.Login)
|
||||
}
|
||||
|
||||
func TestSanitizedReadCloser(t *testing.T) {
|
||||
data := []byte(`the quick brown fox\njumped over the lazy dog\t`)
|
||||
rc := sanitizedReadCloser(io.NopCloser(bytes.NewReader(data)))
|
||||
assert.NoError(t, iotest.TestReader(rc, data))
|
||||
}
|
||||
|
|
@ -34,10 +34,11 @@ var updaterEnabled = ""
|
|||
type exitCode int
|
||||
|
||||
const (
|
||||
exitOK exitCode = 0
|
||||
exitError exitCode = 1
|
||||
exitCancel exitCode = 2
|
||||
exitAuth exitCode = 4
|
||||
exitOK exitCode = 0
|
||||
exitError exitCode = 1
|
||||
exitCancel exitCode = 2
|
||||
exitAuth exitCode = 4
|
||||
exitPending exitCode = 8
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
@ -113,6 +114,8 @@ func mainRun() exitCode {
|
|||
var authError *root.AuthError
|
||||
if err == cmdutil.SilentError {
|
||||
return exitError
|
||||
} else if err == cmdutil.PendingError {
|
||||
return exitPending
|
||||
} else if cmdutil.IsUserCancellation(err) {
|
||||
if errors.Is(err, terminal.InterruptErr) {
|
||||
// ensure the next shell prompt will start on its own line
|
||||
|
|
|
|||
|
|
@ -197,10 +197,10 @@ kiss b github-cli && kiss i github-cli
|
|||
|
||||
### Nix/NixOS
|
||||
|
||||
Nix/NixOS users can install from [nixpkgs](https://search.nixos.org/packages?show=gitAndTools.gh&query=gh&from=0&size=30&sort=relevance&channel=20.03#disabled):
|
||||
Nix/NixOS users can install from [nixpkgs](https://search.nixos.org/packages?query=gh&sort=relevance&show=gh):
|
||||
|
||||
```bash
|
||||
nix-env -iA nixos.gitAndTools.gh
|
||||
nix-env -iA nixos.gh
|
||||
```
|
||||
|
||||
### openSUSE Tumbleweed
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Installation from source
|
||||
|
||||
1. Verify that you have Go 1.19+ installed
|
||||
1. Verify that you have Go 1.21+ installed
|
||||
|
||||
```sh
|
||||
$ go version
|
||||
|
|
|
|||
27
go.mod
27
go.mod
|
|
@ -1,15 +1,15 @@
|
|||
module github.com/cli/cli/v2
|
||||
|
||||
go 1.19
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/MakeNowJust/heredoc v1.0.0
|
||||
github.com/briandowns/spinner v1.18.1
|
||||
github.com/cenkalti/backoff/v4 v4.2.1
|
||||
github.com/charmbracelet/glamour v0.5.1-0.20220727184942-e70ff2d969da
|
||||
github.com/charmbracelet/glamour v0.6.0
|
||||
github.com/charmbracelet/lipgloss v0.5.0
|
||||
github.com/cli/go-gh/v2 v2.0.1
|
||||
github.com/cli/go-gh/v2 v2.3.0
|
||||
github.com/cli/oauth v1.0.1
|
||||
github.com/cli/safeexec v1.0.1
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2
|
||||
|
|
@ -30,16 +30,16 @@ require (
|
|||
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38
|
||||
github.com/opentracing/opentracing-go v1.1.0
|
||||
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d
|
||||
github.com/shurcooL/githubv4 v0.0.0-20230424031643-6cea62ecd5a9
|
||||
github.com/shurcooL/githubv4 v0.0.0-20230704064427-599ae7bbf278
|
||||
github.com/sourcegraph/jsonrpc2 v0.1.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/zalando/go-keyring v0.2.3
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
|
||||
golang.org/x/crypto v0.12.0
|
||||
golang.org/x/sync v0.1.0
|
||||
golang.org/x/term v0.7.0
|
||||
golang.org/x/text v0.9.0
|
||||
golang.org/x/term v0.11.0
|
||||
golang.org/x/text v0.12.0
|
||||
google.golang.org/grpc v1.53.0
|
||||
google.golang.org/protobuf v1.28.1
|
||||
gopkg.in/h2non/gock.v1 v1.1.2
|
||||
|
|
@ -49,6 +49,7 @@ require (
|
|||
require (
|
||||
github.com/alecthomas/chroma v0.10.0 // indirect
|
||||
github.com/alessio/shellescape v1.4.1 // indirect
|
||||
github.com/aymanbagabas/go-osc52 v1.0.3 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/cli/browser v1.2.0 // indirect
|
||||
github.com/cli/shurcooL-graphql v0.0.3 // indirect
|
||||
|
|
@ -68,20 +69,20 @@ require (
|
|||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.20 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.21 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.12.0 // indirect
|
||||
github.com/muesli/termenv v0.13.0 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 // indirect
|
||||
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect
|
||||
github.com/yuin/goldmark v1.4.13 // indirect
|
||||
github.com/yuin/goldmark v1.5.2 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.1 // indirect
|
||||
golang.org/x/net v0.9.0 // indirect
|
||||
golang.org/x/sys v0.8.0 // indirect
|
||||
golang.org/x/net v0.14.0 // indirect
|
||||
golang.org/x/sys v0.11.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
)
|
||||
|
|
|
|||
55
go.sum
55
go.sum
|
|
@ -8,14 +8,16 @@ github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbf
|
|||
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
|
||||
github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
|
||||
github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
|
||||
github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg=
|
||||
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gLVvwY=
|
||||
github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/charmbracelet/glamour v0.5.1-0.20220727184942-e70ff2d969da h1:FGz53GWQRiKQ/5xUsoCCkewSQIC7u81Scaxx2nUy3nM=
|
||||
github.com/charmbracelet/glamour v0.5.1-0.20220727184942-e70ff2d969da/go.mod h1:HXz79SMFnF9arKxqeoHWxmo1BhplAH7wehlRhKQIL94=
|
||||
github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc=
|
||||
github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc=
|
||||
github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
|
||||
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
|
||||
github.com/cli/browser v1.0.0/go.mod h1:IEWkHYbLjkhtjwwWlwTHW2lGxeS5gezEQBMLTwDHf5Q=
|
||||
|
|
@ -23,8 +25,8 @@ github.com/cli/browser v1.2.0 h1:yvU7e9qf97kZqGFX6n2zJPHsmSObY9ske+iCvKelvXg=
|
|||
github.com/cli/browser v1.2.0/go.mod h1:xFFnXLVcAyW9ni0cuo6NnrbCP75JxJ0RO7VtCBiH/oI=
|
||||
github.com/cli/crypto v0.0.0-20210929142629-6be313f59b03 h1:3f4uHLfWx4/WlnMPXGai03eoWAI+oGHJwr+5OXfxCr8=
|
||||
github.com/cli/crypto v0.0.0-20210929142629-6be313f59b03/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
github.com/cli/go-gh/v2 v2.0.1 h1:W4L7C5xT9CwkcqTsUzBhFhRKGpek9oRtdDLku2Hku+E=
|
||||
github.com/cli/go-gh/v2 v2.0.1/go.mod h1:zWab1jRnJ0Ug8qRjsZHFk/Oq51ZWuhSxRL2FDUDgQWk=
|
||||
github.com/cli/go-gh/v2 v2.3.0 h1:FAQAP4PaWSAJf4VSxFEIYDQ1oBIs+bKB4GXQAiRr2sQ=
|
||||
github.com/cli/go-gh/v2 v2.3.0/go.mod h1:6WBUuf7LUVAc+eXYYX/nYTYURRc6M03K9cJNwBKvwT0=
|
||||
github.com/cli/oauth v1.0.1 h1:pXnTFl/qUegXHK531Dv0LNjW4mLx626eS42gnzfXJPA=
|
||||
github.com/cli/oauth v1.0.1/go.mod h1:qd/FX8ZBD6n1sVNQO3aIdRxeu5LGw9WhKnYhIIoC2A4=
|
||||
github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
|
||||
|
|
@ -91,6 +93,7 @@ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA
|
|||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
|
|
@ -112,16 +115,14 @@ github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
|
|||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/microcosm-cc/bluemonday v1.0.19/go.mod h1:QNzV2UbLK2/53oIIwTOyLUSABMkjZ4tqiyC1g/DyqxE=
|
||||
github.com/microcosm-cc/bluemonday v1.0.20 h1:flpzsq4KU3QIYAYGV/szUat7H+GPOXR0B2JU5A1Wp8Y=
|
||||
github.com/microcosm-cc/bluemonday v1.0.20/go.mod h1:yfBmMi8mxvaZut3Yytv+jTXRY8mxyjJ0/kQBTElld50=
|
||||
github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
|
||||
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.11.0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
|
||||
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
|
||||
github.com/muesli/termenv v0.12.0 h1:KuQRUE3PgxRFWhq4gHvZtPSLCGDqM5q/cYr1pZ39ytc=
|
||||
github.com/muesli/termenv v0.12.0/go.mod h1:WCCv32tusQ/EEZ5S8oUIIrC/nIuBcxCVqlN4Xfkv+7A=
|
||||
github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0=
|
||||
github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
|
||||
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38 h1:0FrBxrkJ0hVembTb/e4EU5Ml6vLcOusAqymmYISg5Uo=
|
||||
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38/go.mod h1:saF2fIVw4banK0H4+/EuqfFLpRnoy5S+ECwTOCcRcSU=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
|
||||
|
|
@ -140,10 +141,10 @@ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
|||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shurcooL/githubv4 v0.0.0-20230424031643-6cea62ecd5a9 h1:nCBaIs5/R0HFP5+aPW/SzFUF8z0oKuCXmuDmHWaxzjY=
|
||||
github.com/shurcooL/githubv4 v0.0.0-20230424031643-6cea62ecd5a9/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo=
|
||||
github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 h1:B1PEwpArrNp4dkQrfxh/abbBAOZBVp0ds+fBEOUOqOc=
|
||||
github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
|
||||
github.com/shurcooL/githubv4 v0.0.0-20230704064427-599ae7bbf278 h1:kdEGVAV4sO46DPtb8k793jiecUEhaX9ixoIBt41HEGU=
|
||||
github.com/shurcooL/githubv4 v0.0.0-20230704064427-599ae7bbf278/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8=
|
||||
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0=
|
||||
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE=
|
||||
github.com/sourcegraph/jsonrpc2 v0.1.0 h1:ohJHjZ+PcaLxDUjqk2NC3tIGsVa5bXThe1ZheSXOjuk=
|
||||
github.com/sourcegraph/jsonrpc2 v0.1.0/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo=
|
||||
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
|
||||
|
|
@ -164,9 +165,9 @@ github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:Buzhfgf
|
|||
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
|
||||
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU=
|
||||
github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
|
||||
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
|
||||
github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms=
|
||||
|
|
@ -175,13 +176,14 @@ golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2
|
|||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/oauth2 v0.4.0 h1:NF0gk8LVPg1Ml7SSbGyySuoxdsXitj7TvgvuRxIMc/M=
|
||||
golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
|
@ -196,36 +198,37 @@ golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w=
|
||||
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
|
||||
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
package api
|
||||
|
||||
// For descriptions of service interfaces, see:
|
||||
// - https://online.visualstudio.com/api/swagger (for visualstudio.com)
|
||||
// - https://docs.github.com/en/rest/reference/repos (for api.github.com)
|
||||
// - https://github.com/github/github/blob/master/app/api/codespaces.rb (for vscs_internal)
|
||||
// TODO(adonovan): replace the last link with a public doc URL when available.
|
||||
|
||||
// TODO(adonovan): a possible reorganization would be to split this
|
||||
// file into three internal packages, one per backend service, and to
|
||||
// file into two internal packages, one per backend service, and to
|
||||
// rename api.API to github.Client:
|
||||
//
|
||||
// - github.GetUser(github.Client)
|
||||
|
|
@ -20,7 +19,6 @@ package api
|
|||
// - codespaces.GetToken(Client, login, name)
|
||||
// - codespaces.List(Client, user)
|
||||
// - codespaces.Start(Client, token, codespace)
|
||||
// - visualstudio.GetRegionLocation(http.Client) // no dependency on github
|
||||
//
|
||||
// This would make the meaning of each operation clearer.
|
||||
|
||||
|
|
@ -34,6 +32,7 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
|
@ -42,13 +41,14 @@ import (
|
|||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
)
|
||||
|
||||
const (
|
||||
githubServer = "https://github.com"
|
||||
githubAPI = "https://api.github.com"
|
||||
vscsAPI = "https://online.visualstudio.com"
|
||||
defaultAPIURL = "https://api.github.com"
|
||||
defaultServerURL = "https://github.com"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -60,31 +60,40 @@ const (
|
|||
|
||||
// API is the interface to the codespace service.
|
||||
type API struct {
|
||||
client httpClient
|
||||
vscsAPI string
|
||||
client func() (*http.Client, error)
|
||||
githubAPI string
|
||||
githubServer string
|
||||
retryBackoff time.Duration
|
||||
}
|
||||
|
||||
type httpClient interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
// New creates a new API client connecting to the configured endpoints with the HTTP client.
|
||||
func New(serverURL, apiURL, vscsURL string, httpClient httpClient) *API {
|
||||
if serverURL == "" {
|
||||
serverURL = githubServer
|
||||
}
|
||||
func New(f *cmdutil.Factory) *API {
|
||||
apiURL := os.Getenv("GITHUB_API_URL")
|
||||
if apiURL == "" {
|
||||
apiURL = githubAPI
|
||||
cfg, err := f.Config()
|
||||
if err != nil {
|
||||
// fallback to the default api endpoint
|
||||
apiURL = defaultAPIURL
|
||||
} else {
|
||||
host, _ := cfg.Authentication().DefaultHost()
|
||||
apiURL = ghinstance.RESTPrefix(host)
|
||||
}
|
||||
}
|
||||
if vscsURL == "" {
|
||||
vscsURL = vscsAPI
|
||||
|
||||
serverURL := os.Getenv("GITHUB_SERVER_URL")
|
||||
if serverURL == "" {
|
||||
cfg, err := f.Config()
|
||||
if err != nil {
|
||||
// fallback to the default server endpoint
|
||||
serverURL = defaultServerURL
|
||||
} else {
|
||||
host, _ := cfg.Authentication().DefaultHost()
|
||||
serverURL = ghinstance.HostPrefix(host)
|
||||
}
|
||||
}
|
||||
|
||||
return &API{
|
||||
client: httpClient,
|
||||
vscsAPI: strings.TrimSuffix(vscsURL, "/"),
|
||||
client: f.HttpClient,
|
||||
githubAPI: strings.TrimSuffix(apiURL, "/"),
|
||||
githubServer: strings.TrimSuffix(serverURL, "/"),
|
||||
retryBackoff: 100 * time.Millisecond,
|
||||
|
|
@ -97,6 +106,11 @@ type User struct {
|
|||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// ServerURL returns the server url (not the API url), such as https://github.com
|
||||
func (a *API) ServerURL() string {
|
||||
return a.githubServer
|
||||
}
|
||||
|
||||
// GetUser returns the user associated with the given token.
|
||||
func (a *API) GetUser(ctx context.Context) (*User, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, a.githubAPI+"/user", nil)
|
||||
|
|
@ -1119,7 +1133,13 @@ func (a *API) do(ctx context.Context, req *http.Request, spanName string) (*http
|
|||
span, ctx := opentracing.StartSpanFromContext(ctx, spanName)
|
||||
defer span.Finish()
|
||||
req = req.WithContext(ctx)
|
||||
return a.client.Do(req)
|
||||
|
||||
httpClient, err := a.client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return httpClient.Do(req)
|
||||
}
|
||||
|
||||
// setHeaders sets the required headers for the API.
|
||||
|
|
@ -1139,6 +1159,6 @@ func (a *API) withRetry(f func() (*http.Response, error)) (*http.Response, error
|
|||
if resp.StatusCode < 500 {
|
||||
return resp, nil
|
||||
}
|
||||
return nil, errors.New("retry")
|
||||
return nil, fmt.Errorf("received response with status code %d", resp.StatusCode)
|
||||
}, backoff.WithMaxRetries(bo, 3))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,16 @@ package api
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
)
|
||||
|
||||
func generateCodespaceList(start int, end int) []*Codespace {
|
||||
|
|
@ -126,13 +130,187 @@ func createFakeCreateEndpointServer(t *testing.T, wantStatus int) *httptest.Serv
|
|||
}))
|
||||
}
|
||||
|
||||
func createHttpClient() (*http.Client, error) {
|
||||
return &http.Client{}, nil
|
||||
}
|
||||
|
||||
func TestNew_APIURL_dotcomConfig(t *testing.T) {
|
||||
t.Setenv("GITHUB_API_URL", "")
|
||||
t.Setenv("GITHUB_SERVER_URL", "https://github.com")
|
||||
cfg := &config.ConfigMock{
|
||||
AuthenticationFunc: func() *config.AuthConfig {
|
||||
return &config.AuthConfig{}
|
||||
},
|
||||
}
|
||||
f := &cmdutil.Factory{
|
||||
Config: func() (config.Config, error) {
|
||||
return cfg, nil
|
||||
},
|
||||
}
|
||||
api := New(f)
|
||||
|
||||
if api.githubAPI != "https://api.github.com" {
|
||||
t.Fatalf("expected https://api.github.com, got %s", api.githubAPI)
|
||||
}
|
||||
if len(cfg.AuthenticationCalls()) != 1 {
|
||||
t.Fatalf("API url was not pulled from the config")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_APIURL_customConfig(t *testing.T) {
|
||||
t.Setenv("GITHUB_API_URL", "")
|
||||
t.Setenv("GITHUB_SERVER_URL", "https://github.mycompany.com")
|
||||
cfg := &config.ConfigMock{
|
||||
AuthenticationFunc: func() *config.AuthConfig {
|
||||
authCfg := &config.AuthConfig{}
|
||||
authCfg.SetDefaultHost("github.mycompany.com", "GH_HOST")
|
||||
return authCfg
|
||||
},
|
||||
}
|
||||
f := &cmdutil.Factory{
|
||||
Config: func() (config.Config, error) {
|
||||
return cfg, nil
|
||||
},
|
||||
}
|
||||
api := New(f)
|
||||
|
||||
if api.githubAPI != "https://github.mycompany.com/api/v3" {
|
||||
t.Fatalf("expected https://github.mycompany.com/api/v3, got %s", api.githubAPI)
|
||||
}
|
||||
if len(cfg.AuthenticationCalls()) != 1 {
|
||||
t.Fatalf("API url was not pulled from the config")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_APIURL_env(t *testing.T) {
|
||||
t.Setenv("GITHUB_API_URL", "https://api.mycompany.com")
|
||||
t.Setenv("GITHUB_SERVER_URL", "https://mycompany.com")
|
||||
cfg := &config.ConfigMock{
|
||||
AuthenticationFunc: func() *config.AuthConfig {
|
||||
return &config.AuthConfig{}
|
||||
},
|
||||
}
|
||||
f := &cmdutil.Factory{
|
||||
Config: func() (config.Config, error) {
|
||||
return cfg, nil
|
||||
},
|
||||
}
|
||||
api := New(f)
|
||||
|
||||
if api.githubAPI != "https://api.mycompany.com" {
|
||||
t.Fatalf("expected https://api.mycompany.com, got %s", api.githubAPI)
|
||||
}
|
||||
if len(cfg.AuthenticationCalls()) != 0 {
|
||||
t.Fatalf("Configuration was checked instead of using the GITHUB_API_URL environment variable")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_APIURL_dotcomFallback(t *testing.T) {
|
||||
t.Setenv("GITHUB_API_URL", "")
|
||||
f := &cmdutil.Factory{
|
||||
Config: func() (config.Config, error) {
|
||||
return nil, errors.New("Failed to load")
|
||||
},
|
||||
}
|
||||
api := New(f)
|
||||
|
||||
if api.githubAPI != "https://api.github.com" {
|
||||
t.Fatalf("expected https://api.github.com, got %s", api.githubAPI)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_ServerURL_dotcomConfig(t *testing.T) {
|
||||
t.Setenv("GITHUB_SERVER_URL", "")
|
||||
t.Setenv("GITHUB_API_URL", "https://api.github.com")
|
||||
cfg := &config.ConfigMock{
|
||||
AuthenticationFunc: func() *config.AuthConfig {
|
||||
return &config.AuthConfig{}
|
||||
},
|
||||
}
|
||||
f := &cmdutil.Factory{
|
||||
Config: func() (config.Config, error) {
|
||||
return cfg, nil
|
||||
},
|
||||
}
|
||||
api := New(f)
|
||||
|
||||
if api.githubServer != "https://github.com" {
|
||||
t.Fatalf("expected https://github.com, got %s", api.githubServer)
|
||||
}
|
||||
if len(cfg.AuthenticationCalls()) != 1 {
|
||||
t.Fatalf("Server url was not pulled from the config")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_ServerURL_customConfig(t *testing.T) {
|
||||
t.Setenv("GITHUB_SERVER_URL", "")
|
||||
t.Setenv("GITHUB_API_URL", "https://github.mycompany.com/api/v3")
|
||||
cfg := &config.ConfigMock{
|
||||
AuthenticationFunc: func() *config.AuthConfig {
|
||||
authCfg := &config.AuthConfig{}
|
||||
authCfg.SetDefaultHost("github.mycompany.com", "GH_HOST")
|
||||
return authCfg
|
||||
},
|
||||
}
|
||||
f := &cmdutil.Factory{
|
||||
Config: func() (config.Config, error) {
|
||||
return cfg, nil
|
||||
},
|
||||
}
|
||||
api := New(f)
|
||||
|
||||
if api.githubServer != "https://github.mycompany.com" {
|
||||
t.Fatalf("expected https://github.mycompany.com, got %s", api.githubServer)
|
||||
}
|
||||
if len(cfg.AuthenticationCalls()) != 1 {
|
||||
t.Fatalf("Server url was not pulled from the config")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_ServerURL_env(t *testing.T) {
|
||||
t.Setenv("GITHUB_SERVER_URL", "https://mycompany.com")
|
||||
t.Setenv("GITHUB_API_URL", "https://api.mycompany.com")
|
||||
cfg := &config.ConfigMock{
|
||||
AuthenticationFunc: func() *config.AuthConfig {
|
||||
return &config.AuthConfig{}
|
||||
},
|
||||
}
|
||||
f := &cmdutil.Factory{
|
||||
Config: func() (config.Config, error) {
|
||||
return cfg, nil
|
||||
},
|
||||
}
|
||||
api := New(f)
|
||||
|
||||
if api.githubServer != "https://mycompany.com" {
|
||||
t.Fatalf("expected https://mycompany.com, got %s", api.githubServer)
|
||||
}
|
||||
if len(cfg.AuthenticationCalls()) != 0 {
|
||||
t.Fatalf("Configuration was checked instead of using the GITHUB_SERVER_URL environment variable")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_ServerURL_dotcomFallback(t *testing.T) {
|
||||
t.Setenv("GITHUB_SERVER_URL", "")
|
||||
f := &cmdutil.Factory{
|
||||
Config: func() (config.Config, error) {
|
||||
return nil, errors.New("Failed to load")
|
||||
},
|
||||
}
|
||||
api := New(f)
|
||||
|
||||
if api.githubServer != "https://github.com" {
|
||||
t.Fatalf("expected https://github.com, got %s", api.githubServer)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateCodespaces(t *testing.T) {
|
||||
svr := createFakeCreateEndpointServer(t, http.StatusCreated)
|
||||
defer svr.Close()
|
||||
|
||||
api := API{
|
||||
githubAPI: svr.URL,
|
||||
client: &http.Client{},
|
||||
client: createHttpClient,
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
|
|
@ -161,7 +339,7 @@ func TestCreateCodespaces_displayName(t *testing.T) {
|
|||
|
||||
api := API{
|
||||
githubAPI: svr.URL,
|
||||
client: &http.Client{},
|
||||
client: createHttpClient,
|
||||
}
|
||||
|
||||
retentionPeriod := 0
|
||||
|
|
@ -186,7 +364,7 @@ func TestCreateCodespaces_Pending(t *testing.T) {
|
|||
|
||||
api := API{
|
||||
githubAPI: svr.URL,
|
||||
client: &http.Client{},
|
||||
client: createHttpClient,
|
||||
retryBackoff: 0,
|
||||
}
|
||||
|
||||
|
|
@ -213,7 +391,7 @@ func TestListCodespaces_limited(t *testing.T) {
|
|||
|
||||
api := API{
|
||||
githubAPI: svr.URL,
|
||||
client: &http.Client{},
|
||||
client: createHttpClient,
|
||||
}
|
||||
ctx := context.TODO()
|
||||
codespaces, err := api.ListCodespaces(ctx, ListCodespacesOptions{Limit: 200})
|
||||
|
|
@ -238,7 +416,7 @@ func TestListCodespaces_unlimited(t *testing.T) {
|
|||
|
||||
api := API{
|
||||
githubAPI: svr.URL,
|
||||
client: &http.Client{},
|
||||
client: createHttpClient,
|
||||
}
|
||||
ctx := context.TODO()
|
||||
codespaces, err := api.ListCodespaces(ctx, ListCodespacesOptions{})
|
||||
|
|
@ -329,7 +507,7 @@ func runRepoSearchTest(t *testing.T, searchText, wantQueryText, wantSort, wantMa
|
|||
|
||||
api := API{
|
||||
githubAPI: svr.URL,
|
||||
client: &http.Client{},
|
||||
client: createHttpClient,
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
|
@ -374,7 +552,7 @@ func TestRetries(t *testing.T) {
|
|||
t.Cleanup(srv.Close)
|
||||
a := &API{
|
||||
githubAPI: srv.URL,
|
||||
client: &http.Client{},
|
||||
client: createHttpClient,
|
||||
}
|
||||
cs, err := a.GetCodespace(context.Background(), "test", false)
|
||||
if err != nil {
|
||||
|
|
@ -562,7 +740,7 @@ func TestAPI_EditCodespace(t *testing.T) {
|
|||
defer svr.Close()
|
||||
|
||||
a := &API{
|
||||
client: &http.Client{},
|
||||
client: createHttpClient,
|
||||
githubAPI: svr.URL,
|
||||
}
|
||||
got, err := a.EditCodespace(tt.args.ctx, tt.args.codespaceName, tt.args.params)
|
||||
|
|
@ -602,7 +780,7 @@ func TestAPI_EditCodespacePendingOperation(t *testing.T) {
|
|||
defer svr.Close()
|
||||
|
||||
a := &API{
|
||||
client: &http.Client{},
|
||||
client: createHttpClient,
|
||||
githubAPI: svr.URL,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,14 @@ type logger interface {
|
|||
Printf(f string, v ...interface{})
|
||||
}
|
||||
|
||||
type TimeoutError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (e *TimeoutError) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
// ConnectToLiveshare waits for a Codespace to become running,
|
||||
// and connects to it using a Live Share session.
|
||||
func ConnectToLiveshare(ctx context.Context, progress progressIndicator, sessionLogger logger, apiClient apiClient, codespace *api.Codespace) (*liveshare.Session, error) {
|
||||
|
|
@ -63,15 +71,15 @@ func ConnectToLiveshare(ctx context.Context, progress progressIndicator, session
|
|||
return nil
|
||||
}
|
||||
|
||||
return errors.New("codespace not ready yet")
|
||||
return &TimeoutError{message: "codespace not ready yet"}
|
||||
}, backoff.WithContext(expBackoff, ctx))
|
||||
if err != nil {
|
||||
var permErr *backoff.PermanentError
|
||||
if errors.As(err, &permErr) {
|
||||
return nil, err
|
||||
var timeoutErr *TimeoutError
|
||||
if errors.As(err, &timeoutErr) {
|
||||
return nil, errors.New("timed out while waiting for the codespace to start")
|
||||
}
|
||||
|
||||
return nil, errors.New("timed out while waiting for the codespace to start")
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type Detector interface {
|
||||
|
|
@ -27,11 +28,13 @@ type PullRequestFeatures struct {
|
|||
// the checkRunCount, checkRunCountsByState, statusContextCount and stausContextCountsByState
|
||||
// fields on the StatusCheckRollupContextConnection
|
||||
CheckRunAndStatusContextCounts bool
|
||||
CheckRunEvent bool
|
||||
}
|
||||
|
||||
var allPullRequestFeatures = PullRequestFeatures{
|
||||
MergeQueue: true,
|
||||
CheckRunAndStatusContextCounts: true,
|
||||
CheckRunEvent: true,
|
||||
}
|
||||
|
||||
type RepositoryFeatures struct {
|
||||
|
|
@ -111,8 +114,26 @@ func (d *detector) PullRequestFeatures() (PullRequestFeatures, error) {
|
|||
} `graphql:"StatusCheckRollupContextConnection: __type(name: \"StatusCheckRollupContextConnection\")"`
|
||||
}
|
||||
|
||||
// Break feature detection down into two separate queries because the platform
|
||||
// only supports two `__type` expressions in one query.
|
||||
var pullRequestFeatureDetection2 struct {
|
||||
WorkflowRun struct {
|
||||
Fields []struct {
|
||||
Name string
|
||||
} `graphql:"fields(includeDeprecated: true)"`
|
||||
} `graphql:"WorkflowRun: __type(name: \"WorkflowRun\")"`
|
||||
}
|
||||
|
||||
gql := api.NewClientFromHTTP(d.httpClient)
|
||||
if err := gql.Query(d.host, "PullRequest_fields", &pullRequestFeatureDetection, nil); err != nil {
|
||||
|
||||
var wg errgroup.Group
|
||||
wg.Go(func() error {
|
||||
return gql.Query(d.host, "PullRequest_fields", &pullRequestFeatureDetection, nil)
|
||||
})
|
||||
wg.Go(func() error {
|
||||
return gql.Query(d.host, "PullRequest_fields2", &pullRequestFeatureDetection2, nil)
|
||||
})
|
||||
if err := wg.Wait(); err != nil {
|
||||
return PullRequestFeatures{}, err
|
||||
}
|
||||
|
||||
|
|
@ -132,6 +153,12 @@ func (d *detector) PullRequestFeatures() (PullRequestFeatures, error) {
|
|||
}
|
||||
}
|
||||
|
||||
for _, field := range pullRequestFeatureDetection2.WorkflowRun.Fields {
|
||||
if field.Name == "event" {
|
||||
features.CheckRunEvent = true
|
||||
}
|
||||
}
|
||||
|
||||
return features, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ func TestPullRequestFeatures(t *testing.T) {
|
|||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "github.com with merge queue and status check counts by state",
|
||||
name: "github.com with all features",
|
||||
hostname: "github.com",
|
||||
queryResponse: map[string]string{
|
||||
`query PullRequest_fields\b`: heredoc.Doc(`
|
||||
|
|
@ -104,10 +104,21 @@ func TestPullRequestFeatures(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}`),
|
||||
`query PullRequest_fields2\b`: heredoc.Doc(`
|
||||
{
|
||||
"data": {
|
||||
"WorkflowRun": {
|
||||
"fields": [
|
||||
{"name": "event"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
wantFeatures: PullRequestFeatures{
|
||||
MergeQueue: true,
|
||||
CheckRunAndStatusContextCounts: true,
|
||||
CheckRunEvent: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
|
@ -131,15 +142,26 @@ func TestPullRequestFeatures(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}`),
|
||||
`query PullRequest_fields2\b`: heredoc.Doc(`
|
||||
{
|
||||
"data": {
|
||||
"WorkflowRun": {
|
||||
"fields": [
|
||||
{"name": "event"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
wantFeatures: PullRequestFeatures{
|
||||
MergeQueue: false,
|
||||
CheckRunAndStatusContextCounts: true,
|
||||
CheckRunEvent: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "GHE with merge queue and status check counts by state",
|
||||
name: "GHE with all features",
|
||||
hostname: "git.my.org",
|
||||
queryResponse: map[string]string{
|
||||
`query PullRequest_fields\b`: heredoc.Doc(`
|
||||
|
|
@ -161,15 +183,26 @@ func TestPullRequestFeatures(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}`),
|
||||
`query PullRequest_fields2\b`: heredoc.Doc(`
|
||||
{
|
||||
"data": {
|
||||
"WorkflowRun": {
|
||||
"fields": [
|
||||
{"name": "event"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
wantFeatures: PullRequestFeatures{
|
||||
MergeQueue: true,
|
||||
CheckRunAndStatusContextCounts: true,
|
||||
CheckRunEvent: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "GHE without merge queue and status check counts by state",
|
||||
name: "GHE with no features",
|
||||
hostname: "git.my.org",
|
||||
queryResponse: map[string]string{
|
||||
`query PullRequest_fields\b`: heredoc.Doc(`
|
||||
|
|
@ -183,10 +216,19 @@ func TestPullRequestFeatures(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}`),
|
||||
`query PullRequest_fields2\b`: heredoc.Doc(`
|
||||
{
|
||||
"data": {
|
||||
"WorkflowRun": {
|
||||
"fields": []
|
||||
}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
wantFeatures: PullRequestFeatures{
|
||||
MergeQueue: false,
|
||||
CheckRunAndStatusContextCounts: false,
|
||||
CheckRunEvent: false,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,132 +2,74 @@ package prompter
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/surveyext"
|
||||
ghPrompter "github.com/cli/go-gh/v2/pkg/prompter"
|
||||
)
|
||||
|
||||
//go:generate moq -rm -out prompter_mock.go . Prompter
|
||||
type Prompter interface {
|
||||
// generic prompts from go-gh
|
||||
Select(string, string, []string) (int, error)
|
||||
MultiSelect(string, []string, []string) ([]int, error)
|
||||
MultiSelect(prompt string, defaults []string, options []string) ([]int, error)
|
||||
Input(string, string) (string, error)
|
||||
InputHostname() (string, error)
|
||||
Password(string) (string, error)
|
||||
AuthToken() (string, error)
|
||||
Confirm(string, bool) (bool, error)
|
||||
|
||||
// gh specific prompts
|
||||
AuthToken() (string, error)
|
||||
ConfirmDeletion(string) error
|
||||
InputHostname() (string, error)
|
||||
MarkdownEditor(string, string, bool) (string, error)
|
||||
}
|
||||
|
||||
type fileWriter interface {
|
||||
io.Writer
|
||||
Fd() uintptr
|
||||
}
|
||||
|
||||
type fileReader interface {
|
||||
io.Reader
|
||||
Fd() uintptr
|
||||
}
|
||||
|
||||
func New(editorCmd string, stdin fileReader, stdout fileWriter, stderr io.Writer) Prompter {
|
||||
func New(editorCmd string, stdin ghPrompter.FileReader, stdout ghPrompter.FileWriter, stderr ghPrompter.FileWriter) Prompter {
|
||||
return &surveyPrompter{
|
||||
editorCmd: editorCmd,
|
||||
prompter: ghPrompter.New(stdin, stdout, stderr),
|
||||
stdin: stdin,
|
||||
stdout: stdout,
|
||||
stderr: stderr,
|
||||
editorCmd: editorCmd,
|
||||
}
|
||||
}
|
||||
|
||||
type surveyPrompter struct {
|
||||
prompter *ghPrompter.Prompter
|
||||
stdin ghPrompter.FileReader
|
||||
stdout ghPrompter.FileWriter
|
||||
stderr ghPrompter.FileWriter
|
||||
editorCmd string
|
||||
stdin fileReader
|
||||
stdout fileWriter
|
||||
stderr io.Writer
|
||||
}
|
||||
|
||||
// LatinMatchingFilter returns whether the value matches the input filter.
|
||||
// The strings are compared normalized in case.
|
||||
// The filter's diactritics are kept as-is, but the value's are normalized,
|
||||
// so that a missing diactritic in the filter still returns a result.
|
||||
func LatinMatchingFilter(filter, value string, index int) bool {
|
||||
filter = strings.ToLower(filter)
|
||||
value = strings.ToLower(value)
|
||||
|
||||
// include this option if it matches.
|
||||
return strings.Contains(value, filter) || strings.Contains(text.RemoveDiacritics(value), filter)
|
||||
func (p *surveyPrompter) Select(prompt, defaultValue string, options []string) (int, error) {
|
||||
return p.prompter.Select(prompt, defaultValue, options)
|
||||
}
|
||||
|
||||
func (p *surveyPrompter) Select(message, defaultValue string, options []string) (result int, err error) {
|
||||
q := &survey.Select{
|
||||
Message: message,
|
||||
Options: options,
|
||||
PageSize: 20,
|
||||
Filter: LatinMatchingFilter,
|
||||
}
|
||||
|
||||
if defaultValue != "" {
|
||||
// in some situations, defaultValue ends up not being a valid option; do
|
||||
// not set default in that case as it will make survey panic
|
||||
for _, o := range options {
|
||||
if o == defaultValue {
|
||||
q.Default = defaultValue
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = p.ask(q, &result)
|
||||
|
||||
return
|
||||
func (p *surveyPrompter) MultiSelect(prompt string, defaultValues, options []string) ([]int, error) {
|
||||
return p.prompter.MultiSelect(prompt, defaultValues, options)
|
||||
}
|
||||
|
||||
func (p *surveyPrompter) MultiSelect(message string, defaultValues, options []string) (result []int, err error) {
|
||||
q := &survey.MultiSelect{
|
||||
Message: message,
|
||||
Options: options,
|
||||
PageSize: 20,
|
||||
Filter: LatinMatchingFilter,
|
||||
}
|
||||
|
||||
if len(defaultValues) > 0 {
|
||||
// TODO I don't actually know that this is needed, just being extra cautious
|
||||
validatedDefault := []string{}
|
||||
for _, x := range defaultValues {
|
||||
for _, y := range options {
|
||||
if x == y {
|
||||
validatedDefault = append(validatedDefault, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
q.Default = validatedDefault
|
||||
}
|
||||
|
||||
err = p.ask(q, &result)
|
||||
|
||||
return
|
||||
func (p *surveyPrompter) Input(prompt, defaultValue string) (string, error) {
|
||||
return p.prompter.Input(prompt, defaultValue)
|
||||
}
|
||||
|
||||
func (p *surveyPrompter) ask(q survey.Prompt, response interface{}, opts ...survey.AskOpt) error {
|
||||
opts = append(opts, survey.WithStdio(p.stdin, p.stdout, p.stderr))
|
||||
err := survey.AskOne(q, response, opts...)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("could not prompt: %w", err)
|
||||
func (p *surveyPrompter) Password(prompt string) (string, error) {
|
||||
return p.prompter.Password(prompt)
|
||||
}
|
||||
|
||||
func (p *surveyPrompter) Input(prompt, defaultValue string) (result string, err error) {
|
||||
err = p.ask(&survey.Input{
|
||||
Message: prompt,
|
||||
Default: defaultValue,
|
||||
}, &result)
|
||||
func (p *surveyPrompter) Confirm(prompt string, defaultValue bool) (bool, error) {
|
||||
return p.prompter.Confirm(prompt, defaultValue)
|
||||
}
|
||||
|
||||
return
|
||||
func (p *surveyPrompter) AuthToken() (string, error) {
|
||||
var result string
|
||||
err := p.ask(&survey.Password{
|
||||
Message: "Paste your authentication token:",
|
||||
}, &result, survey.WithValidator(survey.Required))
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (p *surveyPrompter) ConfirmDeletion(requiredValue string) error {
|
||||
|
|
@ -146,54 +88,38 @@ func (p *surveyPrompter) ConfirmDeletion(requiredValue string) error {
|
|||
}))
|
||||
}
|
||||
|
||||
func (p *surveyPrompter) InputHostname() (result string, err error) {
|
||||
err = p.ask(
|
||||
func (p *surveyPrompter) InputHostname() (string, error) {
|
||||
var result string
|
||||
err := p.ask(
|
||||
&survey.Input{
|
||||
Message: "GHE hostname:",
|
||||
}, &result, survey.WithValidator(func(v interface{}) error {
|
||||
return ghinstance.HostnameValidator(v.(string))
|
||||
}))
|
||||
|
||||
return
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (p *surveyPrompter) Password(prompt string) (result string, err error) {
|
||||
err = p.ask(&survey.Password{
|
||||
Message: prompt,
|
||||
}, &result)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (p *surveyPrompter) Confirm(prompt string, defaultValue bool) (result bool, err error) {
|
||||
err = p.ask(&survey.Confirm{
|
||||
Message: prompt,
|
||||
Default: defaultValue,
|
||||
}, &result)
|
||||
|
||||
return
|
||||
}
|
||||
func (p *surveyPrompter) MarkdownEditor(message, defaultValue string, blankAllowed bool) (result string, err error) {
|
||||
|
||||
err = p.ask(&surveyext.GhEditor{
|
||||
func (p *surveyPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) {
|
||||
var result string
|
||||
err := p.ask(&surveyext.GhEditor{
|
||||
BlankAllowed: blankAllowed,
|
||||
EditorCommand: p.editorCmd,
|
||||
Editor: &survey.Editor{
|
||||
Message: message,
|
||||
Message: prompt,
|
||||
Default: defaultValue,
|
||||
FileName: "*.md",
|
||||
HideDefault: true,
|
||||
AppendDefault: true,
|
||||
},
|
||||
}, &result)
|
||||
|
||||
return
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (p *surveyPrompter) AuthToken() (result string, err error) {
|
||||
err = p.ask(&survey.Password{
|
||||
Message: "Paste your authentication token:",
|
||||
}, &result, survey.WithValidator(survey.Required))
|
||||
|
||||
return
|
||||
func (p *surveyPrompter) ask(q survey.Prompt, response interface{}, opts ...survey.AskOpt) error {
|
||||
opts = append(opts, survey.WithStdio(p.stdin, p.stdout, p.stderr))
|
||||
err := survey.AskOne(q, response, opts...)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,78 +0,0 @@
|
|||
package prompter
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFilterDiacritics(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
filter string
|
||||
value string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "exact match no diacritics",
|
||||
filter: "Mikelis",
|
||||
value: "Mikelis",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "exact match no diacritics",
|
||||
filter: "Mikelis",
|
||||
value: "Mikelis",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "exact match diacritics",
|
||||
filter: "Miķelis",
|
||||
value: "Miķelis",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "partial match diacritics",
|
||||
filter: "Miķe",
|
||||
value: "Miķelis",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "exact match diacritics in value",
|
||||
filter: "Mikelis",
|
||||
value: "Miķelis",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "partial match diacritics in filter",
|
||||
filter: "Miķe",
|
||||
value: "Miķelis",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "no match when removing diacritics in filter",
|
||||
filter: "Mielis",
|
||||
value: "Mikelis",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "no match when removing diacritics in value",
|
||||
filter: "Mikelis",
|
||||
value: "Mielis",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "no match diacritics in filter",
|
||||
filter: "Miķelis",
|
||||
value: "Mikelis",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, LatinMatchingFilter(tt.filter, tt.value, 0), tt.want)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -5,10 +5,132 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
ghPrompter "github.com/cli/go-gh/v2/pkg/prompter"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test helpers
|
||||
func NewMockPrompter(t *testing.T) *MockPrompter {
|
||||
m := &MockPrompter{
|
||||
t: t,
|
||||
PrompterMock: *ghPrompter.NewMock(t),
|
||||
authTokenStubs: []authTokenStub{},
|
||||
confirmDeletionStubs: []confirmDeletionStub{},
|
||||
inputHostnameStubs: []inputHostnameStub{},
|
||||
markdownEditorStubs: []markdownEditorStub{},
|
||||
}
|
||||
t.Cleanup(m.Verify)
|
||||
return m
|
||||
}
|
||||
|
||||
type MockPrompter struct {
|
||||
t *testing.T
|
||||
ghPrompter.PrompterMock
|
||||
authTokenStubs []authTokenStub
|
||||
confirmDeletionStubs []confirmDeletionStub
|
||||
inputHostnameStubs []inputHostnameStub
|
||||
markdownEditorStubs []markdownEditorStub
|
||||
}
|
||||
|
||||
type authTokenStub struct {
|
||||
fn func() (string, error)
|
||||
}
|
||||
|
||||
type confirmDeletionStub struct {
|
||||
prompt string
|
||||
fn func(string) error
|
||||
}
|
||||
|
||||
type inputHostnameStub struct {
|
||||
fn func() (string, error)
|
||||
}
|
||||
|
||||
type markdownEditorStub struct {
|
||||
prompt string
|
||||
fn func(string, string, bool) (string, error)
|
||||
}
|
||||
|
||||
func (m *MockPrompter) AuthToken() (string, error) {
|
||||
var s authTokenStub
|
||||
if len(m.authTokenStubs) == 0 {
|
||||
return "", NoSuchPromptErr("AuthToken")
|
||||
}
|
||||
s = m.authTokenStubs[0]
|
||||
m.authTokenStubs = m.authTokenStubs[1:len(m.authTokenStubs)]
|
||||
return s.fn()
|
||||
}
|
||||
|
||||
func (m *MockPrompter) ConfirmDeletion(prompt string) error {
|
||||
var s confirmDeletionStub
|
||||
if len(m.confirmDeletionStubs) == 0 {
|
||||
return NoSuchPromptErr("ConfirmDeletion")
|
||||
}
|
||||
s = m.confirmDeletionStubs[0]
|
||||
m.confirmDeletionStubs = m.confirmDeletionStubs[1:len(m.confirmDeletionStubs)]
|
||||
return s.fn(prompt)
|
||||
}
|
||||
|
||||
func (m *MockPrompter) InputHostname() (string, error) {
|
||||
var s inputHostnameStub
|
||||
if len(m.inputHostnameStubs) == 0 {
|
||||
return "", NoSuchPromptErr("InputHostname")
|
||||
}
|
||||
s = m.inputHostnameStubs[0]
|
||||
m.inputHostnameStubs = m.inputHostnameStubs[1:len(m.inputHostnameStubs)]
|
||||
return s.fn()
|
||||
}
|
||||
|
||||
func (m *MockPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) {
|
||||
var s markdownEditorStub
|
||||
if len(m.markdownEditorStubs) == 0 {
|
||||
return "", NoSuchPromptErr(prompt)
|
||||
}
|
||||
s = m.markdownEditorStubs[0]
|
||||
m.markdownEditorStubs = m.markdownEditorStubs[1:len(m.markdownEditorStubs)]
|
||||
if s.prompt != prompt {
|
||||
return "", NoSuchPromptErr(prompt)
|
||||
}
|
||||
return s.fn(prompt, defaultValue, blankAllowed)
|
||||
}
|
||||
|
||||
func (m *MockPrompter) RegisterAuthToken(stub func() (string, error)) {
|
||||
m.authTokenStubs = append(m.authTokenStubs, authTokenStub{fn: stub})
|
||||
}
|
||||
|
||||
func (m *MockPrompter) RegisterConfirmDeletion(prompt string, stub func(string) error) {
|
||||
m.confirmDeletionStubs = append(m.confirmDeletionStubs, confirmDeletionStub{prompt: prompt, fn: stub})
|
||||
}
|
||||
|
||||
func (m *MockPrompter) RegisterInputHostname(stub func() (string, error)) {
|
||||
m.inputHostnameStubs = append(m.inputHostnameStubs, inputHostnameStub{fn: stub})
|
||||
}
|
||||
|
||||
func (m *MockPrompter) RegisterMarkdownEditor(prompt string, stub func(string, string, bool) (string, error)) {
|
||||
m.markdownEditorStubs = append(m.markdownEditorStubs, markdownEditorStub{prompt: prompt, fn: stub})
|
||||
}
|
||||
|
||||
func (m *MockPrompter) Verify() {
|
||||
errs := []string{}
|
||||
if len(m.authTokenStubs) > 0 {
|
||||
errs = append(errs, "AuthToken")
|
||||
}
|
||||
if len(m.confirmDeletionStubs) > 0 {
|
||||
errs = append(errs, "ConfirmDeletion")
|
||||
}
|
||||
if len(m.inputHostnameStubs) > 0 {
|
||||
errs = append(errs, "inputHostname")
|
||||
}
|
||||
if len(m.markdownEditorStubs) > 0 {
|
||||
errs = append(errs, "markdownEditorStubs")
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
m.t.Helper()
|
||||
m.t.Errorf("%d unmatched calls to %s", len(errs), strings.Join(errs, ","))
|
||||
}
|
||||
}
|
||||
|
||||
func AssertOptions(t *testing.T, expected, actual []string) {
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func IndexFor(options []string, answer string) (int, error) {
|
||||
for ix, a := range options {
|
||||
|
|
@ -19,267 +141,10 @@ func IndexFor(options []string, answer string) (int, error) {
|
|||
return -1, NoSuchAnswerErr(answer)
|
||||
}
|
||||
|
||||
func AssertOptions(t *testing.T, expected, actual []string) {
|
||||
assert.Equal(t, expected, actual)
|
||||
func NoSuchPromptErr(prompt string) error {
|
||||
return fmt.Errorf("no such prompt '%s'", prompt)
|
||||
}
|
||||
|
||||
func NoSuchAnswerErr(answer string) error {
|
||||
return fmt.Errorf("no such answer '%s'", answer)
|
||||
}
|
||||
|
||||
func NoSuchPromptErr(prompt string) error {
|
||||
return fmt.Errorf("no such prompt '%s'", prompt)
|
||||
}
|
||||
|
||||
type SelectStub struct {
|
||||
Prompt string
|
||||
ExpectedOpts []string
|
||||
Fn func(string, string, []string) (int, error)
|
||||
}
|
||||
|
||||
type InputStub struct {
|
||||
Prompt string
|
||||
Fn func(string, string) (string, error)
|
||||
}
|
||||
|
||||
type ConfirmStub struct {
|
||||
Prompt string
|
||||
Fn func(string, bool) (bool, error)
|
||||
}
|
||||
|
||||
type MultiSelectStub struct {
|
||||
Prompt string
|
||||
ExpectedOpts []string
|
||||
Fn func(string, []string, []string) ([]int, error)
|
||||
}
|
||||
|
||||
type InputHostnameStub struct {
|
||||
Fn func() (string, error)
|
||||
}
|
||||
|
||||
type PasswordStub struct {
|
||||
Prompt string
|
||||
Fn func(string) (string, error)
|
||||
}
|
||||
|
||||
type AuthTokenStub struct {
|
||||
Fn func() (string, error)
|
||||
}
|
||||
|
||||
type ConfirmDeletionStub struct {
|
||||
Prompt string
|
||||
Fn func(string) error
|
||||
}
|
||||
|
||||
type MockPrompter struct {
|
||||
PrompterMock
|
||||
t *testing.T
|
||||
SelectStubs []SelectStub
|
||||
InputStubs []InputStub
|
||||
ConfirmStubs []ConfirmStub
|
||||
MultiSelectStubs []MultiSelectStub
|
||||
InputHostnameStubs []InputHostnameStub
|
||||
PasswordStubs []PasswordStub
|
||||
AuthTokenStubs []AuthTokenStub
|
||||
ConfirmDeletionStubs []ConfirmDeletionStub
|
||||
}
|
||||
|
||||
func NewMockPrompter(t *testing.T) *MockPrompter {
|
||||
m := &MockPrompter{
|
||||
t: t,
|
||||
SelectStubs: []SelectStub{},
|
||||
InputStubs: []InputStub{},
|
||||
ConfirmStubs: []ConfirmStub{},
|
||||
MultiSelectStubs: []MultiSelectStub{},
|
||||
InputHostnameStubs: []InputHostnameStub{},
|
||||
PasswordStubs: []PasswordStub{},
|
||||
AuthTokenStubs: []AuthTokenStub{},
|
||||
ConfirmDeletionStubs: []ConfirmDeletionStub{},
|
||||
}
|
||||
|
||||
t.Cleanup(m.Verify)
|
||||
|
||||
m.SelectFunc = func(p, d string, opts []string) (int, error) {
|
||||
var s SelectStub
|
||||
|
||||
if len(m.SelectStubs) > 0 {
|
||||
s = m.SelectStubs[0]
|
||||
m.SelectStubs = m.SelectStubs[1:len(m.SelectStubs)]
|
||||
} else {
|
||||
return -1, NoSuchPromptErr(p)
|
||||
}
|
||||
|
||||
if s.Prompt != p {
|
||||
return -1, NoSuchPromptErr(p)
|
||||
}
|
||||
|
||||
AssertOptions(m.t, s.ExpectedOpts, opts)
|
||||
|
||||
return s.Fn(p, d, opts)
|
||||
}
|
||||
|
||||
m.MultiSelectFunc = func(p string, d, opts []string) ([]int, error) {
|
||||
var s MultiSelectStub
|
||||
if len(m.MultiSelectStubs) > 0 {
|
||||
s = m.MultiSelectStubs[0]
|
||||
m.MultiSelectStubs = m.MultiSelectStubs[1:len(m.MultiSelectStubs)]
|
||||
} else {
|
||||
return []int{}, NoSuchPromptErr(p)
|
||||
}
|
||||
|
||||
if s.Prompt != p {
|
||||
return []int{}, NoSuchPromptErr(p)
|
||||
}
|
||||
|
||||
AssertOptions(m.t, s.ExpectedOpts, opts)
|
||||
|
||||
return s.Fn(p, d, opts)
|
||||
}
|
||||
|
||||
m.InputFunc = func(p, d string) (string, error) {
|
||||
var s InputStub
|
||||
|
||||
if len(m.InputStubs) > 0 {
|
||||
s = m.InputStubs[0]
|
||||
m.InputStubs = m.InputStubs[1:len(m.InputStubs)]
|
||||
} else {
|
||||
return "", NoSuchPromptErr(p)
|
||||
}
|
||||
|
||||
if s.Prompt != p {
|
||||
return "", NoSuchPromptErr(p)
|
||||
}
|
||||
|
||||
return s.Fn(p, d)
|
||||
}
|
||||
|
||||
m.ConfirmFunc = func(p string, d bool) (bool, error) {
|
||||
var s ConfirmStub
|
||||
|
||||
if len(m.ConfirmStubs) > 0 {
|
||||
s = m.ConfirmStubs[0]
|
||||
m.ConfirmStubs = m.ConfirmStubs[1:len(m.ConfirmStubs)]
|
||||
} else {
|
||||
return false, NoSuchPromptErr(p)
|
||||
}
|
||||
|
||||
if s.Prompt != p {
|
||||
return false, NoSuchPromptErr(p)
|
||||
}
|
||||
|
||||
return s.Fn(p, d)
|
||||
}
|
||||
|
||||
m.InputHostnameFunc = func() (string, error) {
|
||||
var s InputHostnameStub
|
||||
|
||||
if len(m.InputHostnameStubs) > 0 {
|
||||
s = m.InputHostnameStubs[0]
|
||||
m.InputHostnameStubs = m.InputHostnameStubs[1:len(m.InputHostnameStubs)]
|
||||
} else {
|
||||
return "", NoSuchPromptErr("InputHostname")
|
||||
}
|
||||
|
||||
return s.Fn()
|
||||
}
|
||||
|
||||
m.PasswordFunc = func(p string) (string, error) {
|
||||
var s PasswordStub
|
||||
|
||||
if len(m.PasswordStubs) > 0 {
|
||||
s = m.PasswordStubs[0]
|
||||
m.PasswordStubs = m.PasswordStubs[1:len(m.PasswordStubs)]
|
||||
} else {
|
||||
return "", NoSuchPromptErr(p)
|
||||
}
|
||||
|
||||
if s.Prompt != p {
|
||||
return "", NoSuchPromptErr(p)
|
||||
}
|
||||
|
||||
return s.Fn(p)
|
||||
}
|
||||
|
||||
m.AuthTokenFunc = func() (string, error) {
|
||||
var s AuthTokenStub
|
||||
|
||||
if len(m.AuthTokenStubs) > 0 {
|
||||
s = m.AuthTokenStubs[0]
|
||||
m.AuthTokenStubs = m.AuthTokenStubs[1:len(m.AuthTokenStubs)]
|
||||
} else {
|
||||
return "", NoSuchPromptErr("AuthToken")
|
||||
}
|
||||
|
||||
return s.Fn()
|
||||
}
|
||||
|
||||
m.ConfirmDeletionFunc = func(p string) error {
|
||||
var s ConfirmDeletionStub
|
||||
|
||||
if len(m.ConfirmDeletionStubs) > 0 {
|
||||
s = m.ConfirmDeletionStubs[0]
|
||||
m.ConfirmDeletionStubs = m.ConfirmDeletionStubs[1:len(m.ConfirmDeletionStubs)]
|
||||
} else {
|
||||
return NoSuchPromptErr("ConfirmDeletion")
|
||||
}
|
||||
|
||||
return s.Fn(p)
|
||||
}
|
||||
|
||||
// TODO MarkdownEditor(string, string, bool) (string, error)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *MockPrompter) RegisterSelect(prompt string, opts []string, stub func(_, _ string, _ []string) (int, error)) {
|
||||
m.SelectStubs = append(m.SelectStubs, SelectStub{
|
||||
Prompt: prompt,
|
||||
ExpectedOpts: opts,
|
||||
Fn: stub})
|
||||
}
|
||||
|
||||
func (m *MockPrompter) RegisterMultiSelect(prompt string, d, opts []string, stub func(_ string, _, _ []string) ([]int, error)) {
|
||||
m.MultiSelectStubs = append(m.MultiSelectStubs, MultiSelectStub{
|
||||
Prompt: prompt,
|
||||
ExpectedOpts: opts,
|
||||
Fn: stub})
|
||||
}
|
||||
|
||||
func (m *MockPrompter) RegisterInput(prompt string, stub func(_, _ string) (string, error)) {
|
||||
m.InputStubs = append(m.InputStubs, InputStub{Prompt: prompt, Fn: stub})
|
||||
}
|
||||
|
||||
func (m *MockPrompter) RegisterConfirm(prompt string, stub func(_ string, _ bool) (bool, error)) {
|
||||
m.ConfirmStubs = append(m.ConfirmStubs, ConfirmStub{Prompt: prompt, Fn: stub})
|
||||
}
|
||||
|
||||
func (m *MockPrompter) RegisterInputHostname(stub func() (string, error)) {
|
||||
m.InputHostnameStubs = append(m.InputHostnameStubs, InputHostnameStub{Fn: stub})
|
||||
}
|
||||
|
||||
func (m *MockPrompter) RegisterPassword(prompt string, stub func(string) (string, error)) {
|
||||
m.PasswordStubs = append(m.PasswordStubs, PasswordStub{Fn: stub})
|
||||
}
|
||||
|
||||
func (m *MockPrompter) RegisterConfirmDeletion(prompt string, stub func(string) error) {
|
||||
m.ConfirmDeletionStubs = append(m.ConfirmDeletionStubs, ConfirmDeletionStub{Prompt: prompt, Fn: stub})
|
||||
}
|
||||
|
||||
func (m *MockPrompter) Verify() {
|
||||
errs := []string{}
|
||||
if len(m.SelectStubs) > 0 {
|
||||
errs = append(errs, "Select")
|
||||
}
|
||||
if len(m.InputStubs) > 0 {
|
||||
errs = append(errs, "Input")
|
||||
}
|
||||
if len(m.ConfirmStubs) > 0 {
|
||||
errs = append(errs, "Confirm")
|
||||
}
|
||||
// TODO other prompt types
|
||||
|
||||
if len(errs) > 0 {
|
||||
m.t.Helper()
|
||||
m.t.Errorf("%d unmatched calls to %s", len(errs), strings.Join(errs, ","))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,14 +6,10 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/cli/go-gh/v2/pkg/text"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/runes"
|
||||
"golang.org/x/text/transform"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
var whitespaceRE = regexp.MustCompile(`\s+`)
|
||||
|
|
@ -79,16 +75,5 @@ func DisplayURL(urlStr string) string {
|
|||
|
||||
// RemoveDiacritics returns the input value without "diacritics", or accent marks
|
||||
func RemoveDiacritics(value string) string {
|
||||
// Mn = "Mark, nonspacing" unicode character category
|
||||
removeMnTransfomer := runes.Remove(runes.In(unicode.Mn))
|
||||
|
||||
// 1/ Decompose the text into characters and diacritical marks,
|
||||
// 2/ Remove the diacriticals marks
|
||||
// 3/ Recompose the text
|
||||
t := transform.Chain(norm.NFD, removeMnTransfomer, norm.NFC)
|
||||
normalized, _, err := transform.String(t, value)
|
||||
if err != nil {
|
||||
return value
|
||||
}
|
||||
return normalized
|
||||
return text.RemoveDiacritics(value)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,57 +54,3 @@ func TestFuzzyAgoAbbr(t *testing.T) {
|
|||
assert.Equal(t, expected, fuzzy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveDiacritics(t *testing.T) {
|
||||
tests := [][]string{
|
||||
// no diacritics
|
||||
{"e", "e"},
|
||||
{"و", "و"},
|
||||
{"И", "И"},
|
||||
{"ж", "ж"},
|
||||
{"私", "私"},
|
||||
{"万", "万"},
|
||||
|
||||
// diacritics test sets
|
||||
{"à", "a"},
|
||||
{"é", "e"},
|
||||
{"è", "e"},
|
||||
{"ô", "o"},
|
||||
{"ᾳ", "α"},
|
||||
{"εͅ", "ε"},
|
||||
{"ῃ", "η"},
|
||||
{"ιͅ", "ι"},
|
||||
|
||||
{"ؤ", "و"},
|
||||
|
||||
{"ā", "a"},
|
||||
{"č", "c"},
|
||||
{"ģ", "g"},
|
||||
{"ķ", "k"},
|
||||
{"ņ", "n"},
|
||||
{"š", "s"},
|
||||
{"ž", "z"},
|
||||
|
||||
{"ŵ", "w"},
|
||||
{"ŷ", "y"},
|
||||
{"ä", "a"},
|
||||
{"ÿ", "y"},
|
||||
{"á", "a"},
|
||||
{"ẁ", "w"},
|
||||
{"ỳ", "y"},
|
||||
{"ō", "o"},
|
||||
|
||||
// full words
|
||||
{"Miķelis", "Mikelis"},
|
||||
{"François", "Francois"},
|
||||
{"žluťoučký", "zlutoucky"},
|
||||
{"învățătorița", "invatatorita"},
|
||||
{"Kękę przy łóżku", "Keke przy łozku"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(RemoveDiacritics(tt[0]), func(t *testing.T) {
|
||||
assert.Equal(t, tt[1], RemoveDiacritics(tt[0]))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ func actionsExplainer(cs *iostreams.ColorScheme) string {
|
|||
header := cs.Bold("Welcome to GitHub Actions on the command line.")
|
||||
runHeader := cs.Bold("Interacting with workflow runs")
|
||||
workflowHeader := cs.Bold("Interacting with workflow files")
|
||||
cacheHeader := cs.Bold("Interacting with the Actions cache")
|
||||
|
||||
return heredoc.Docf(`
|
||||
%s
|
||||
|
|
@ -49,5 +50,12 @@ func actionsExplainer(cs *iostreams.ColorScheme) string {
|
|||
gh workflow run: Trigger a workflow_dispatch run for a workflow file
|
||||
|
||||
To see more help, run 'gh help workflow <subcommand>'
|
||||
`, header, runHeader, workflowHeader)
|
||||
|
||||
%s
|
||||
gh cache list: List all the caches saved in Actions for a repository
|
||||
gh cache delete: Delete one or all saved caches in Actions for a repository
|
||||
|
||||
To see more help, run 'gh help cache <subcommand>'
|
||||
|
||||
`, header, runHeader, workflowHeader, cacheHeader)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,9 +17,10 @@ type SetOptions struct {
|
|||
Config func() (config.Config, error)
|
||||
IO *iostreams.IOStreams
|
||||
|
||||
Name string
|
||||
Expansion string
|
||||
IsShell bool
|
||||
Name string
|
||||
Expansion string
|
||||
IsShell bool
|
||||
OverwriteExisting bool
|
||||
|
||||
validAliasName func(string) bool
|
||||
validAliasExpansion func(string) bool
|
||||
|
|
@ -86,6 +87,7 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
|
|||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&opts.IsShell, "shell", "s", false, "Declare an alias to be passed through a shell interpreter")
|
||||
cmd.Flags().BoolVar(&opts.OverwriteExisting, "clobber", false, "Overwrite existing aliases of the same name")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -104,31 +106,39 @@ func setRun(opts *SetOptions) error {
|
|||
return fmt.Errorf("did not understand expansion: %w", err)
|
||||
}
|
||||
|
||||
isTerminal := opts.IO.IsStdoutTTY()
|
||||
if isTerminal {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "- Adding alias for %s: %s\n", cs.Bold(opts.Name), cs.Bold(expansion))
|
||||
}
|
||||
|
||||
if opts.IsShell && !strings.HasPrefix(expansion, "!") {
|
||||
expansion = "!" + expansion
|
||||
}
|
||||
|
||||
isTerminal := opts.IO.IsStdoutTTY()
|
||||
if isTerminal {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "- Creating alias for %s: %s\n", cs.Bold(opts.Name), cs.Bold(expansion))
|
||||
}
|
||||
|
||||
var existingAlias bool
|
||||
if _, err := aliasCfg.Get(opts.Name); err == nil {
|
||||
existingAlias = true
|
||||
}
|
||||
|
||||
if !opts.validAliasName(opts.Name) {
|
||||
return fmt.Errorf("could not create alias: %q is already a gh command, extension, or alias", opts.Name)
|
||||
if !existingAlias {
|
||||
return fmt.Errorf("%s Could not create alias %s: already a gh command or extension",
|
||||
cs.FailureIcon(),
|
||||
cs.Bold(opts.Name))
|
||||
}
|
||||
|
||||
if existingAlias && !opts.OverwriteExisting {
|
||||
return fmt.Errorf("%s Could not create alias %s: name already taken, use the --clobber flag to overwrite it",
|
||||
cs.FailureIcon(),
|
||||
cs.Bold(opts.Name),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if !opts.validAliasExpansion(expansion) {
|
||||
return fmt.Errorf("could not create alias: %s does not correspond to a gh command, extension, or alias", expansion)
|
||||
}
|
||||
|
||||
successMsg := fmt.Sprintf("%s Added alias.", cs.SuccessIcon())
|
||||
if oldExpansion, err := aliasCfg.Get(opts.Name); err == nil {
|
||||
successMsg = fmt.Sprintf("%s Changed alias %s from %s to %s",
|
||||
cs.SuccessIcon(),
|
||||
cs.Bold(opts.Name),
|
||||
cs.Bold(oldExpansion),
|
||||
cs.Bold(expansion),
|
||||
)
|
||||
return fmt.Errorf("%s Could not create alias %s: expansion does not correspond to a gh command, extension, or alias",
|
||||
cs.FailureIcon(),
|
||||
cs.Bold(opts.Name))
|
||||
}
|
||||
|
||||
aliasCfg.Add(opts.Name, expansion)
|
||||
|
|
@ -138,6 +148,13 @@ func setRun(opts *SetOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
successMsg := fmt.Sprintf("%s Added alias %s", cs.SuccessIcon(), cs.Bold(opts.Name))
|
||||
if existingAlias && opts.OverwriteExisting {
|
||||
successMsg = fmt.Sprintf("%s Changed alias %s",
|
||||
cs.WarningIcon(),
|
||||
cs.Bold(opts.Name))
|
||||
}
|
||||
|
||||
if isTerminal {
|
||||
fmt.Fprintln(opts.IO.ErrOut, successMsg)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,314 +2,313 @@ package set
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/pkg/cmd/alias/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/extensions"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/test"
|
||||
"github.com/google/shlex"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func runCommand(cfg config.Config, isTTY bool, cli string, in string) (*test.CmdOut, error) {
|
||||
ios, stdin, stdout, stderr := iostreams.Test()
|
||||
ios.SetStdoutTTY(isTTY)
|
||||
ios.SetStdinTTY(isTTY)
|
||||
ios.SetStderrTTY(isTTY)
|
||||
stdin.WriteString(in)
|
||||
|
||||
factory := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
Config: func() (config.Config, error) {
|
||||
return cfg, nil
|
||||
func TestNewCmdSet(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
output SetOptions
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "no arguments",
|
||||
input: "",
|
||||
wantErr: true,
|
||||
errMsg: "accepts 2 arg(s), received 0",
|
||||
},
|
||||
ExtensionManager: &extensions.ExtensionManagerMock{
|
||||
ListFunc: func() []extensions.Extension {
|
||||
return []extensions.Extension{}
|
||||
{
|
||||
name: "only one argument",
|
||||
input: "name",
|
||||
wantErr: true,
|
||||
errMsg: "accepts 2 arg(s), received 1",
|
||||
},
|
||||
{
|
||||
name: "name and expansion",
|
||||
input: "alias-name alias-expansion",
|
||||
output: SetOptions{
|
||||
Name: "alias-name",
|
||||
Expansion: "alias-expansion",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "shell flag",
|
||||
input: "alias-name alias-expansion --shell",
|
||||
output: SetOptions{
|
||||
Name: "alias-name",
|
||||
Expansion: "alias-expansion",
|
||||
IsShell: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "clobber flag",
|
||||
input: "alias-name alias-expansion --clobber",
|
||||
output: SetOptions{
|
||||
Name: "alias-name",
|
||||
Expansion: "alias-expansion",
|
||||
OverwriteExisting: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
}
|
||||
argv, err := shlex.Split(tt.input)
|
||||
assert.NoError(t, err)
|
||||
var gotOpts *SetOptions
|
||||
cmd := NewCmdSet(f, func(opts *SetOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
cmd.SetArgs(argv)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(&bytes.Buffer{})
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
|
||||
cmd := NewCmdSet(factory, nil)
|
||||
|
||||
// Create fake command structure for testing.
|
||||
rootCmd := &cobra.Command{}
|
||||
rootCmd.AddCommand(cmd)
|
||||
prCmd := &cobra.Command{Use: "pr"}
|
||||
prCmd.AddCommand(&cobra.Command{Use: "checkout"})
|
||||
prCmd.AddCommand(&cobra.Command{Use: "status"})
|
||||
rootCmd.AddCommand(prCmd)
|
||||
issueCmd := &cobra.Command{Use: "issue"}
|
||||
issueCmd.AddCommand(&cobra.Command{Use: "list"})
|
||||
rootCmd.AddCommand(issueCmd)
|
||||
apiCmd := &cobra.Command{Use: "api"}
|
||||
apiCmd.AddCommand(&cobra.Command{Use: "graphql"})
|
||||
rootCmd.AddCommand(apiCmd)
|
||||
|
||||
argv, err := shlex.Split("set " + cli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rootCmd.SetArgs(argv)
|
||||
|
||||
rootCmd.SetIn(stdin)
|
||||
rootCmd.SetOut(io.Discard)
|
||||
rootCmd.SetErr(io.Discard)
|
||||
|
||||
_, err = rootCmd.ExecuteC()
|
||||
return &test.CmdOut{
|
||||
OutBuf: stdout,
|
||||
ErrBuf: stderr,
|
||||
}, err
|
||||
}
|
||||
|
||||
func TestAliasSet_gh_command(t *testing.T) {
|
||||
cfg := config.NewFromString(``)
|
||||
|
||||
_, err := runCommand(cfg, true, "pr 'pr status'", "")
|
||||
assert.EqualError(t, err, `could not create alias: "pr" is already a gh command, extension, or alias`)
|
||||
}
|
||||
|
||||
func TestAliasSet_empty_aliases(t *testing.T) {
|
||||
readConfigs := config.StubWriteConfig(t)
|
||||
|
||||
cfg := config.NewFromString(heredoc.Doc(`
|
||||
aliases:
|
||||
editor: vim
|
||||
`))
|
||||
|
||||
output, err := runCommand(cfg, true, "co 'pr checkout'", "")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
mainBuf := bytes.Buffer{}
|
||||
readConfigs(&mainBuf, io.Discard)
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), "Added alias")
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.String(), "")
|
||||
|
||||
expected := `aliases:
|
||||
co: pr checkout
|
||||
editor: vim
|
||||
`
|
||||
assert.Equal(t, expected, mainBuf.String())
|
||||
}
|
||||
|
||||
func TestAliasSet_existing_alias(t *testing.T) {
|
||||
_ = config.StubWriteConfig(t)
|
||||
|
||||
cfg := config.NewFromString(heredoc.Doc(`
|
||||
aliases:
|
||||
co: pr checkout
|
||||
`))
|
||||
|
||||
output, err := runCommand(cfg, true, "co 'pr checkout -Rcool/repo'", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), "Changed alias.*co.*from.*pr checkout.*to.*pr checkout -Rcool/repo")
|
||||
}
|
||||
|
||||
func TestAliasSet_space_args(t *testing.T) {
|
||||
readConfigs := config.StubWriteConfig(t)
|
||||
|
||||
cfg := config.NewFromString(``)
|
||||
|
||||
output, err := runCommand(cfg, true, `il 'issue list -l "cool story"'`, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
mainBuf := bytes.Buffer{}
|
||||
readConfigs(&mainBuf, io.Discard)
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), `Adding alias for.*il.*issue list -l "cool story"`)
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, mainBuf.String(), `il: issue list -l "cool story"`)
|
||||
}
|
||||
|
||||
func TestAliasSet_arg_processing(t *testing.T) {
|
||||
readConfigs := config.StubWriteConfig(t)
|
||||
|
||||
cases := []struct {
|
||||
Cmd string
|
||||
ExpectedOutputLine string
|
||||
ExpectedConfigLine string
|
||||
}{
|
||||
{`il "issue list"`, "- Adding alias for.*il.*issue list", "il: issue list"},
|
||||
|
||||
{`iz 'issue list'`, "- Adding alias for.*iz.*issue list", "iz: issue list"},
|
||||
|
||||
{`ii 'issue list --author="$1" --label="$2"'`,
|
||||
`- Adding alias for.*ii.*issue list --author="\$1" --label="\$2"`,
|
||||
`ii: issue list --author="\$1" --label="\$2"`},
|
||||
|
||||
{`ix "issue list --author='\$1' --label='\$2'"`,
|
||||
`- Adding alias for.*ix.*issue list --author='\$1' --label='\$2'`,
|
||||
`ix: issue list --author='\$1' --label='\$2'`},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.Cmd, func(t *testing.T) {
|
||||
cfg := config.NewFromString(``)
|
||||
|
||||
output, err := runCommand(cfg, true, c.Cmd, "")
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error running %s: %s", c.Cmd, err)
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantErr {
|
||||
assert.EqualError(t, err, tt.errMsg)
|
||||
return
|
||||
}
|
||||
|
||||
mainBuf := bytes.Buffer{}
|
||||
readConfigs(&mainBuf, io.Discard)
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), c.ExpectedOutputLine)
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, mainBuf.String(), c.ExpectedConfigLine)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.output.Name, gotOpts.Name)
|
||||
assert.Equal(t, tt.output.Expansion, gotOpts.Expansion)
|
||||
assert.Equal(t, tt.output.IsShell, gotOpts.IsShell)
|
||||
assert.Equal(t, tt.output.OverwriteExisting, gotOpts.OverwriteExisting)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAliasSet_init_alias_cfg(t *testing.T) {
|
||||
readConfigs := config.StubWriteConfig(t)
|
||||
|
||||
cfg := config.NewFromString(heredoc.Doc(`
|
||||
editor: vim
|
||||
`))
|
||||
|
||||
output, err := runCommand(cfg, true, "diff 'pr diff'", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
mainBuf := bytes.Buffer{}
|
||||
readConfigs(&mainBuf, io.Discard)
|
||||
|
||||
expected := `editor: vim
|
||||
aliases:
|
||||
diff: pr diff
|
||||
`
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), "Adding alias for.*diff.*pr diff", "Added alias.")
|
||||
assert.Equal(t, expected, mainBuf.String())
|
||||
}
|
||||
|
||||
func TestAliasSet_existing_aliases(t *testing.T) {
|
||||
readConfigs := config.StubWriteConfig(t)
|
||||
|
||||
cfg := config.NewFromString(heredoc.Doc(`
|
||||
aliases:
|
||||
foo: bar
|
||||
`))
|
||||
|
||||
output, err := runCommand(cfg, true, "view 'pr view'", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
mainBuf := bytes.Buffer{}
|
||||
readConfigs(&mainBuf, io.Discard)
|
||||
|
||||
expected := `aliases:
|
||||
foo: bar
|
||||
view: pr view
|
||||
`
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), "Adding alias for.*view.*pr view", "Added alias.")
|
||||
assert.Equal(t, expected, mainBuf.String())
|
||||
|
||||
}
|
||||
|
||||
func TestAliasSet_invalid_command(t *testing.T) {
|
||||
cfg := config.NewFromString(``)
|
||||
|
||||
_, err := runCommand(cfg, true, "co 'pe checkout'", "")
|
||||
assert.EqualError(t, err, "could not create alias: pe checkout does not correspond to a gh command, extension, or alias")
|
||||
}
|
||||
|
||||
func TestShellAlias_flag(t *testing.T) {
|
||||
readConfigs := config.StubWriteConfig(t)
|
||||
|
||||
cfg := config.NewFromString(``)
|
||||
|
||||
output, err := runCommand(cfg, true, "--shell igrep 'gh issue list | grep'", "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
func TestSetRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tty bool
|
||||
opts *SetOptions
|
||||
stdin string
|
||||
wantExpansion string
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
wantErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "creates alias tty",
|
||||
tty: true,
|
||||
opts: &SetOptions{
|
||||
Name: "foo",
|
||||
Expansion: "bar",
|
||||
},
|
||||
wantExpansion: "bar",
|
||||
wantStderr: "- Creating alias for foo: bar\n✓ Added alias foo\n",
|
||||
},
|
||||
{
|
||||
name: "creates alias",
|
||||
opts: &SetOptions{
|
||||
Name: "foo",
|
||||
Expansion: "bar",
|
||||
},
|
||||
wantExpansion: "bar",
|
||||
},
|
||||
{
|
||||
name: "creates shell alias tty",
|
||||
tty: true,
|
||||
opts: &SetOptions{
|
||||
Name: "igrep",
|
||||
Expansion: "!gh issue list | grep",
|
||||
},
|
||||
wantExpansion: "!gh issue list | grep",
|
||||
wantStderr: "- Creating alias for igrep: !gh issue list | grep\n✓ Added alias igrep\n",
|
||||
},
|
||||
{
|
||||
name: "creates shell alias",
|
||||
opts: &SetOptions{
|
||||
Name: "igrep",
|
||||
Expansion: "!gh issue list | grep",
|
||||
},
|
||||
wantExpansion: "!gh issue list | grep",
|
||||
},
|
||||
{
|
||||
name: "creates shell alias using flag tty",
|
||||
tty: true,
|
||||
opts: &SetOptions{
|
||||
Name: "igrep",
|
||||
Expansion: "gh issue list | grep",
|
||||
IsShell: true,
|
||||
},
|
||||
wantExpansion: "!gh issue list | grep",
|
||||
wantStderr: "- Creating alias for igrep: !gh issue list | grep\n✓ Added alias igrep\n",
|
||||
},
|
||||
{
|
||||
name: "creates shell alias using flag",
|
||||
opts: &SetOptions{
|
||||
Name: "igrep",
|
||||
Expansion: "gh issue list | grep",
|
||||
IsShell: true,
|
||||
},
|
||||
wantExpansion: "!gh issue list | grep",
|
||||
},
|
||||
{
|
||||
name: "creates alias where expansion has args tty",
|
||||
tty: true,
|
||||
opts: &SetOptions{
|
||||
Name: "foo",
|
||||
Expansion: "bar baz --author='$1' --label='$2'",
|
||||
},
|
||||
wantExpansion: "bar baz --author='$1' --label='$2'",
|
||||
wantStderr: "- Creating alias for foo: bar baz --author='$1' --label='$2'\n✓ Added alias foo\n",
|
||||
},
|
||||
{
|
||||
name: "creates alias where expansion has args",
|
||||
opts: &SetOptions{
|
||||
Name: "foo",
|
||||
Expansion: "bar baz --author='$1' --label='$2'",
|
||||
},
|
||||
wantExpansion: "bar baz --author='$1' --label='$2'",
|
||||
},
|
||||
{
|
||||
name: "creates alias from stdin tty",
|
||||
tty: true,
|
||||
opts: &SetOptions{
|
||||
Name: "foo",
|
||||
Expansion: "-",
|
||||
},
|
||||
stdin: `bar baz --author="$1" --label="$2"`,
|
||||
wantExpansion: `bar baz --author="$1" --label="$2"`,
|
||||
wantStderr: "- Creating alias for foo: bar baz --author=\"$1\" --label=\"$2\"\n✓ Added alias foo\n",
|
||||
},
|
||||
{
|
||||
name: "creates alias from stdin",
|
||||
opts: &SetOptions{
|
||||
Name: "foo",
|
||||
Expansion: "-",
|
||||
},
|
||||
stdin: `bar baz --author="$1" --label="$2"`,
|
||||
wantExpansion: `bar baz --author="$1" --label="$2"`,
|
||||
},
|
||||
{
|
||||
name: "overwrites existing alias tty",
|
||||
tty: true,
|
||||
opts: &SetOptions{
|
||||
Name: "co",
|
||||
Expansion: "bar",
|
||||
OverwriteExisting: true,
|
||||
},
|
||||
wantExpansion: "bar",
|
||||
wantStderr: "- Creating alias for co: bar\n! Changed alias co\n",
|
||||
},
|
||||
{
|
||||
name: "overwrites existing alias",
|
||||
opts: &SetOptions{
|
||||
Name: "co",
|
||||
Expansion: "bar",
|
||||
OverwriteExisting: true,
|
||||
},
|
||||
wantExpansion: "bar",
|
||||
},
|
||||
{
|
||||
name: "fails when alias name is an existing alias tty",
|
||||
tty: true,
|
||||
opts: &SetOptions{
|
||||
Name: "co",
|
||||
Expansion: "bar",
|
||||
},
|
||||
wantExpansion: "pr checkout",
|
||||
wantErrMsg: "X Could not create alias co: name already taken, use the --clobber flag to overwrite it",
|
||||
wantStderr: "- Creating alias for co: bar\n",
|
||||
},
|
||||
{
|
||||
name: "fails when alias name is an existing alias",
|
||||
opts: &SetOptions{
|
||||
Name: "co",
|
||||
Expansion: "bar",
|
||||
},
|
||||
wantExpansion: "pr checkout",
|
||||
wantErrMsg: "X Could not create alias co: name already taken, use the --clobber flag to overwrite it",
|
||||
},
|
||||
{
|
||||
name: "fails when alias expansion is not an existing command tty",
|
||||
tty: true,
|
||||
opts: &SetOptions{
|
||||
Name: "foo",
|
||||
Expansion: "baz",
|
||||
},
|
||||
wantErrMsg: "X Could not create alias foo: expansion does not correspond to a gh command, extension, or alias",
|
||||
wantStderr: "- Creating alias for foo: baz\n",
|
||||
},
|
||||
{
|
||||
name: "fails when alias expansion is not an existing command",
|
||||
opts: &SetOptions{
|
||||
Name: "foo",
|
||||
Expansion: "baz",
|
||||
},
|
||||
wantErrMsg: "X Could not create alias foo: expansion does not correspond to a gh command, extension, or alias",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rootCmd := &cobra.Command{}
|
||||
barCmd := &cobra.Command{Use: "bar"}
|
||||
barCmd.AddCommand(&cobra.Command{Use: "baz"})
|
||||
rootCmd.AddCommand(barCmd)
|
||||
coCmd := &cobra.Command{Use: "co"}
|
||||
rootCmd.AddCommand(coCmd)
|
||||
|
||||
mainBuf := bytes.Buffer{}
|
||||
readConfigs(&mainBuf, io.Discard)
|
||||
tt.opts.validAliasName = shared.ValidAliasNameFunc(rootCmd)
|
||||
tt.opts.validAliasExpansion = shared.ValidAliasExpansionFunc(rootCmd)
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), "Adding alias for.*igrep")
|
||||
ios, stdin, stdout, stderr := iostreams.Test()
|
||||
ios.SetStdinTTY(tt.tty)
|
||||
ios.SetStdoutTTY(tt.tty)
|
||||
ios.SetStderrTTY(tt.tty)
|
||||
tt.opts.IO = ios
|
||||
|
||||
expected := `aliases:
|
||||
igrep: '!gh issue list | grep'
|
||||
`
|
||||
assert.Equal(t, expected, mainBuf.String())
|
||||
if tt.stdin != "" {
|
||||
fmt.Fprint(stdin, tt.stdin)
|
||||
}
|
||||
|
||||
cfg := config.NewBlankConfig()
|
||||
cfg.WriteFunc = func() error {
|
||||
return nil
|
||||
}
|
||||
tt.opts.Config = func() (config.Config, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
err := setRun(tt.opts)
|
||||
if tt.wantErrMsg != "" {
|
||||
assert.EqualError(t, err, tt.wantErrMsg)
|
||||
writeCalls := cfg.WriteCalls()
|
||||
assert.Equal(t, 0, len(writeCalls))
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
writeCalls := cfg.WriteCalls()
|
||||
assert.Equal(t, 1, len(writeCalls))
|
||||
}
|
||||
|
||||
ac := cfg.Aliases()
|
||||
expansion, _ := ac.Get(tt.opts.Name)
|
||||
assert.Equal(t, tt.wantExpansion, expansion)
|
||||
assert.Equal(t, tt.wantStdout, stdout.String())
|
||||
assert.Equal(t, tt.wantStderr, stderr.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShellAlias_bang(t *testing.T) {
|
||||
readConfigs := config.StubWriteConfig(t)
|
||||
|
||||
cfg := config.NewFromString(``)
|
||||
|
||||
output, err := runCommand(cfg, true, "igrep '!gh issue list | grep'", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
mainBuf := bytes.Buffer{}
|
||||
readConfigs(&mainBuf, io.Discard)
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), "Adding alias for.*igrep")
|
||||
|
||||
expected := `aliases:
|
||||
igrep: '!gh issue list | grep'
|
||||
`
|
||||
assert.Equal(t, expected, mainBuf.String())
|
||||
}
|
||||
|
||||
func TestShellAlias_from_stdin(t *testing.T) {
|
||||
readConfigs := config.StubWriteConfig(t)
|
||||
|
||||
cfg := config.NewFromString(``)
|
||||
|
||||
output, err := runCommand(cfg, true, "users -", `api graphql -F name="$1" -f query='
|
||||
query ($name: String!) {
|
||||
user(login: $name) {
|
||||
name
|
||||
}
|
||||
}'`)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
mainBuf := bytes.Buffer{}
|
||||
readConfigs(&mainBuf, io.Discard)
|
||||
|
||||
//nolint:staticcheck // prefer exact matchers over ExpectLines
|
||||
test.ExpectLines(t, output.Stderr(), "Adding alias for.*users")
|
||||
|
||||
expected := `aliases:
|
||||
users: |-
|
||||
api graphql -F name="$1" -f query='
|
||||
query ($name: String!) {
|
||||
user(login: $name) {
|
||||
name
|
||||
}
|
||||
}'
|
||||
`
|
||||
|
||||
assert.Equal(t, expected, mainBuf.String())
|
||||
}
|
||||
|
||||
func TestShellAlias_getExpansion(t *testing.T) {
|
||||
func TestGetExpansion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want string
|
||||
|
|
|
|||
|
|
@ -30,7 +30,12 @@ import (
|
|||
)
|
||||
|
||||
type ApiOptions struct {
|
||||
IO *iostreams.IOStreams
|
||||
AppVersion string
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Branch func() (string, error)
|
||||
Config func() (config.Config, error)
|
||||
HttpClient func() (*http.Client, error)
|
||||
IO *iostreams.IOStreams
|
||||
|
||||
Hostname string
|
||||
RequestMethod string
|
||||
|
|
@ -47,20 +52,16 @@ type ApiOptions struct {
|
|||
Template string
|
||||
CacheTTL time.Duration
|
||||
FilterOutput string
|
||||
|
||||
Config func() (config.Config, error)
|
||||
HttpClient func() (*http.Client, error)
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Branch func() (string, error)
|
||||
Verbose bool
|
||||
}
|
||||
|
||||
func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command {
|
||||
opts := ApiOptions{
|
||||
IO: f.IOStreams,
|
||||
Config: f.Config,
|
||||
HttpClient: f.HttpClient,
|
||||
AppVersion: f.AppVersion,
|
||||
BaseRepo: f.BaseRepo,
|
||||
Branch: f.Branch,
|
||||
Config: f.Config,
|
||||
IO: f.IOStreams,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
|
@ -209,7 +210,8 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
|
|||
}
|
||||
|
||||
if err := cmdutil.MutuallyExclusive(
|
||||
"only one of `--template`, `--jq`, or `--silent` may be used",
|
||||
"only one of `--template`, `--jq`, `--silent`, or `--verbose` may be used",
|
||||
opts.Verbose,
|
||||
opts.Silent,
|
||||
opts.FilterOutput != "",
|
||||
opts.Template != "",
|
||||
|
|
@ -237,6 +239,7 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
|
|||
cmd.Flags().StringVarP(&opts.Template, "template", "t", "", "Format JSON output using a Go template; see \"gh help formatting\"")
|
||||
cmd.Flags().StringVarP(&opts.FilterOutput, "jq", "q", "", "Query to select values from the response using jq syntax")
|
||||
cmd.Flags().DurationVar(&opts.CacheTTL, "cache", 0, "Cache the response, e.g. \"3600s\", \"60m\", \"1h\"")
|
||||
cmd.Flags().BoolVar(&opts.Verbose, "verbose", false, "Include full HTTP request and response in the output")
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
|
@ -283,12 +286,33 @@ func apiRun(opts *ApiOptions) error {
|
|||
requestHeaders = append(requestHeaders, "Accept: "+previewNamesToMIMETypes(opts.Previews))
|
||||
}
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
cfg, err := opts.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if opts.CacheTTL > 0 {
|
||||
httpClient = api.NewCachedHTTPClient(httpClient, opts.CacheTTL)
|
||||
|
||||
if opts.HttpClient == nil {
|
||||
opts.HttpClient = func() (*http.Client, error) {
|
||||
log := opts.IO.ErrOut
|
||||
if opts.Verbose {
|
||||
log = opts.IO.Out
|
||||
}
|
||||
opts := api.HTTPClientOptions{
|
||||
AppVersion: opts.AppVersion,
|
||||
CacheTTL: opts.CacheTTL,
|
||||
Config: cfg.Authentication(),
|
||||
EnableCache: opts.CacheTTL > 0,
|
||||
Log: log,
|
||||
LogColorize: opts.IO.ColorEnabled(),
|
||||
LogVerboseHTTP: opts.Verbose,
|
||||
SkipAcceptHeaders: true,
|
||||
}
|
||||
return api.NewHTTPClient(opts)
|
||||
}
|
||||
}
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !opts.Silent {
|
||||
|
|
@ -304,10 +328,10 @@ func apiRun(opts *ApiOptions) error {
|
|||
if opts.Silent {
|
||||
bodyWriter = io.Discard
|
||||
}
|
||||
|
||||
cfg, err := opts.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
if opts.Verbose {
|
||||
// httpClient handles output when verbose flag is specified.
|
||||
bodyWriter = io.Discard
|
||||
headersWriter = io.Discard
|
||||
}
|
||||
|
||||
host, _ := cfg.Authentication().DefaultHost()
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ func Test_NewCmdApi(t *testing.T) {
|
|||
CacheTTL: 0,
|
||||
Template: "",
|
||||
FilterOutput: "",
|
||||
Verbose: false,
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
|
|
@ -73,6 +74,7 @@ func Test_NewCmdApi(t *testing.T) {
|
|||
CacheTTL: 0,
|
||||
Template: "",
|
||||
FilterOutput: "",
|
||||
Verbose: false,
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
|
|
@ -94,6 +96,7 @@ func Test_NewCmdApi(t *testing.T) {
|
|||
CacheTTL: 0,
|
||||
Template: "",
|
||||
FilterOutput: "",
|
||||
Verbose: false,
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
|
|
@ -115,6 +118,7 @@ func Test_NewCmdApi(t *testing.T) {
|
|||
CacheTTL: 0,
|
||||
Template: "",
|
||||
FilterOutput: "",
|
||||
Verbose: false,
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
|
|
@ -136,6 +140,7 @@ func Test_NewCmdApi(t *testing.T) {
|
|||
CacheTTL: 0,
|
||||
Template: "",
|
||||
FilterOutput: "",
|
||||
Verbose: false,
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
|
|
@ -157,6 +162,7 @@ func Test_NewCmdApi(t *testing.T) {
|
|||
CacheTTL: 0,
|
||||
Template: "",
|
||||
FilterOutput: "",
|
||||
Verbose: false,
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
|
|
@ -183,6 +189,7 @@ func Test_NewCmdApi(t *testing.T) {
|
|||
CacheTTL: 0,
|
||||
Template: "",
|
||||
FilterOutput: "",
|
||||
Verbose: false,
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
|
|
@ -209,6 +216,7 @@ func Test_NewCmdApi(t *testing.T) {
|
|||
CacheTTL: 0,
|
||||
Template: "",
|
||||
FilterOutput: "",
|
||||
Verbose: false,
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
|
|
@ -235,6 +243,7 @@ func Test_NewCmdApi(t *testing.T) {
|
|||
CacheTTL: 0,
|
||||
Template: "",
|
||||
FilterOutput: "",
|
||||
Verbose: false,
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
|
|
@ -256,6 +265,7 @@ func Test_NewCmdApi(t *testing.T) {
|
|||
CacheTTL: time.Minute * 5,
|
||||
Template: "",
|
||||
FilterOutput: "",
|
||||
Verbose: false,
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
|
|
@ -277,6 +287,7 @@ func Test_NewCmdApi(t *testing.T) {
|
|||
CacheTTL: 0,
|
||||
Template: "hello {{.name}}",
|
||||
FilterOutput: "",
|
||||
Verbose: false,
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
|
|
@ -298,6 +309,7 @@ func Test_NewCmdApi(t *testing.T) {
|
|||
CacheTTL: 0,
|
||||
Template: "",
|
||||
FilterOutput: ".name",
|
||||
Verbose: false,
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
|
|
@ -316,6 +328,28 @@ func Test_NewCmdApi(t *testing.T) {
|
|||
cli: "user --jq .foo -t '{{.foo}}'",
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "with verbose",
|
||||
cli: "user --verbose",
|
||||
wants: ApiOptions{
|
||||
Hostname: "",
|
||||
RequestMethod: "GET",
|
||||
RequestMethodPassed: false,
|
||||
RequestPath: "user",
|
||||
RequestInputFile: "",
|
||||
RawFields: []string(nil),
|
||||
MagicFields: []string(nil),
|
||||
RequestHeaders: []string(nil),
|
||||
ShowResponseHeaders: false,
|
||||
Paginate: false,
|
||||
Silent: false,
|
||||
CacheTTL: 0,
|
||||
Template: "",
|
||||
FilterOutput: "",
|
||||
Verbose: true,
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
@ -352,6 +386,7 @@ func Test_NewCmdApi(t *testing.T) {
|
|||
assert.Equal(t, tt.wants.CacheTTL, opts.CacheTTL)
|
||||
assert.Equal(t, tt.wants.Template, opts.Template)
|
||||
assert.Equal(t, tt.wants.FilterOutput, opts.FilterOutput)
|
||||
assert.Equal(t, tt.wants.Verbose, opts.Verbose)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ func Login(opts *LoginOptions) error {
|
|||
fmt.Fprint(opts.IO.ErrOut, heredoc.Docf(`
|
||||
Tip: you can generate a Personal Access Token here https://%s/settings/tokens
|
||||
The minimum required scopes are %s.
|
||||
`, hostname, scopesSentence(minimumScopes, ghinstance.IsEnterprise(hostname))))
|
||||
`, hostname, scopesSentence(minimumScopes)))
|
||||
|
||||
var err error
|
||||
authToken, err = opts.Prompter.AuthToken()
|
||||
|
|
@ -216,14 +216,10 @@ func Login(opts *LoginOptions) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func scopesSentence(scopes []string, isEnterprise bool) string {
|
||||
func scopesSentence(scopes []string) string {
|
||||
quoted := make([]string, len(scopes))
|
||||
for i, s := range scopes {
|
||||
quoted[i] = fmt.Sprintf("'%s'", s)
|
||||
if s == "workflow" && isEnterprise {
|
||||
// remove when GHE 2.x reaches EOL
|
||||
quoted[i] += " (GHE 3.0+)"
|
||||
}
|
||||
}
|
||||
return strings.Join(quoted, ", ")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,8 +117,7 @@ func TestLogin_ssh(t *testing.T) {
|
|||
|
||||
func Test_scopesSentence(t *testing.T) {
|
||||
type args struct {
|
||||
scopes []string
|
||||
isEnterprise bool
|
||||
scopes []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
@ -128,39 +127,28 @@ func Test_scopesSentence(t *testing.T) {
|
|||
{
|
||||
name: "basic scopes",
|
||||
args: args{
|
||||
scopes: []string{"repo", "read:org"},
|
||||
isEnterprise: false,
|
||||
scopes: []string{"repo", "read:org"},
|
||||
},
|
||||
want: "'repo', 'read:org'",
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
args: args{
|
||||
scopes: []string(nil),
|
||||
isEnterprise: false,
|
||||
scopes: []string(nil),
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "workflow scope for dotcom",
|
||||
name: "workflow scope",
|
||||
args: args{
|
||||
scopes: []string{"repo", "workflow"},
|
||||
isEnterprise: false,
|
||||
scopes: []string{"repo", "workflow"},
|
||||
},
|
||||
want: "'repo', 'workflow'",
|
||||
},
|
||||
{
|
||||
name: "workflow scope for GHE",
|
||||
args: args{
|
||||
scopes: []string{"repo", "workflow"},
|
||||
isEnterprise: true,
|
||||
},
|
||||
want: "'repo', 'workflow' (GHE 3.0+)",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := scopesSentence(tt.args.scopes, tt.args.isEnterprise); got != tt.want {
|
||||
if got := scopesSentence(tt.args.scopes); got != tt.want {
|
||||
t.Errorf("scopesSentence() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ func statusRun(opts *StatusOptions) error {
|
|||
scopesHeader, err := shared.GetScopes(httpClient, hostname, token)
|
||||
if err != nil {
|
||||
addMsg("%s %s: authentication failed", cs.Red("X"), hostname)
|
||||
addMsg("- The %s token in %s is no longer valid.", cs.Bold(hostname), tokenSource)
|
||||
addMsg("- The %s token in %s is invalid.", cs.Bold(hostname), tokenSource)
|
||||
if tokenIsWriteable {
|
||||
addMsg("- To re-authenticate, run: %s %s",
|
||||
cs.Bold("gh auth login -h"), cs.Bold(hostname))
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ func Test_statusRun(t *testing.T) {
|
|||
wantErrOut: heredoc.Doc(`
|
||||
joel.miller
|
||||
X joel.miller: authentication failed
|
||||
- The joel.miller token in GH_CONFIG_DIR/hosts.yml is no longer valid.
|
||||
- The joel.miller token in GH_CONFIG_DIR/hosts.yml is invalid.
|
||||
- To re-authenticate, run: gh auth login -h joel.miller
|
||||
- To forget about this host, run: gh auth logout -h joel.miller
|
||||
|
||||
|
|
|
|||
|
|
@ -178,7 +178,19 @@ func runBrowse(opts *BrowseOptions) error {
|
|||
url := ghrepo.GenerateRepoURL(baseRepo, "%s", section)
|
||||
|
||||
if opts.NoBrowserFlag {
|
||||
_, err := fmt.Fprintln(opts.IO.Out, url)
|
||||
client, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
exist, err := api.RepoExists(api.NewClientFromHTTP(client), baseRepo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exist {
|
||||
return fmt.Errorf("%s doesn't exist", text.DisplayURL(url))
|
||||
}
|
||||
_, err = fmt.Fprintln(opts.IO.Out, url)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -232,6 +232,7 @@ func Test_runBrowse(t *testing.T) {
|
|||
tests := []struct {
|
||||
name string
|
||||
opts BrowseOptions
|
||||
httpStub func(*httpmock.Registry)
|
||||
baseRepo ghrepo.Interface
|
||||
defaultBranch string
|
||||
expectedURL string
|
||||
|
|
@ -432,6 +433,12 @@ func Test_runBrowse(t *testing.T) {
|
|||
SelectorArg: "init.rb:6",
|
||||
NoBrowserFlag: true,
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
httpmock.REST("HEAD", "repos/mislav/will_paginate"),
|
||||
httpmock.StringResponse("{}"),
|
||||
)
|
||||
},
|
||||
baseRepo: ghrepo.New("mislav", "will_paginate"),
|
||||
wantsErr: false,
|
||||
expectedURL: "https://github.com/mislav/will_paginate/blob/3-0-stable/init.rb?plain=1#L6",
|
||||
|
|
@ -556,6 +563,10 @@ func Test_runBrowse(t *testing.T) {
|
|||
reg.StubRepoInfoResponse(tt.baseRepo.RepoOwner(), tt.baseRepo.RepoName(), tt.defaultBranch)
|
||||
}
|
||||
|
||||
if tt.httpStub != nil {
|
||||
tt.httpStub(®)
|
||||
}
|
||||
|
||||
opts := tt.opts
|
||||
opts.IO = ios
|
||||
opts.BaseRepo = func() (ghrepo.Interface, error) {
|
||||
|
|
|
|||
29
pkg/cmd/cache/cache.go
vendored
Normal file
29
pkg/cmd/cache/cache.go
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
cmdDelete "github.com/cli/cli/v2/pkg/cmd/cache/delete"
|
||||
cmdList "github.com/cli/cli/v2/pkg/cmd/cache/list"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewCmdCache(f *cmdutil.Factory) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "cache <command>",
|
||||
Short: "Manage Github Actions caches",
|
||||
Long: "Work with Github Actions caches.",
|
||||
Example: heredoc.Doc(`
|
||||
$ gh cache list
|
||||
$ gh cache delete --all
|
||||
`),
|
||||
GroupID: "actions",
|
||||
}
|
||||
|
||||
cmdutil.EnableRepoOverride(cmd, f)
|
||||
|
||||
cmd.AddCommand(cmdList.NewCmdList(f, nil))
|
||||
cmd.AddCommand(cmdDelete.NewCmdDelete(f, nil))
|
||||
|
||||
return cmd
|
||||
}
|
||||
155
pkg/cmd/cache/delete/delete.go
vendored
Normal file
155
pkg/cmd/cache/delete/delete.go
vendored
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
package delete
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/cmd/cache/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type DeleteOptions struct {
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
HttpClient func() (*http.Client, error)
|
||||
IO *iostreams.IOStreams
|
||||
|
||||
DeleteAll bool
|
||||
Identifier string
|
||||
}
|
||||
|
||||
func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Command {
|
||||
opts := &DeleteOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete [<cache-id>| <cache-key> | --all]",
|
||||
Short: "Delete Github Actions caches",
|
||||
Long: `
|
||||
Delete Github Actions caches.
|
||||
|
||||
Deletion requires authorization with the "repo" scope.
|
||||
`,
|
||||
Example: heredoc.Doc(`
|
||||
# Delete a cache by id
|
||||
$ gh cache delete 1234
|
||||
|
||||
# Delete a cache by key
|
||||
$ gh cache delete cache-key
|
||||
|
||||
# Delete a cache by id in a specific repo
|
||||
$ gh cache delete 1234 --repo cli/cli
|
||||
|
||||
# Delete all caches
|
||||
$ gh cache delete --all
|
||||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support -R/--repo flag
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
|
||||
if err := cmdutil.MutuallyExclusive(
|
||||
"specify only one of cache id, cache key, or --all",
|
||||
opts.DeleteAll, len(args) > 0,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !opts.DeleteAll && len(args) == 0 {
|
||||
return cmdutil.FlagErrorf("must provide either cache id, cache key, or use --all")
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
opts.Identifier = args[0]
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
|
||||
return deleteRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&opts.DeleteAll, "all", "a", false, "Delete all caches")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func deleteRun(opts *DeleteOptions) error {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create http client: %w", err)
|
||||
}
|
||||
client := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
repo, err := opts.BaseRepo()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to determine base repo: %w", err)
|
||||
}
|
||||
|
||||
var toDelete []string
|
||||
if opts.DeleteAll {
|
||||
caches, err := shared.GetCaches(client, repo, shared.GetCachesOptions{Limit: -1})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(caches.ActionsCaches) == 0 {
|
||||
return fmt.Errorf("%s No caches to delete", opts.IO.ColorScheme().FailureIcon())
|
||||
}
|
||||
for _, cache := range caches.ActionsCaches {
|
||||
toDelete = append(toDelete, strconv.Itoa(cache.Id))
|
||||
}
|
||||
} else {
|
||||
toDelete = append(toDelete, opts.Identifier)
|
||||
}
|
||||
|
||||
return deleteCaches(opts, client, repo, toDelete)
|
||||
}
|
||||
|
||||
func deleteCaches(opts *DeleteOptions, client *api.Client, repo ghrepo.Interface, toDelete []string) error {
|
||||
cs := opts.IO.ColorScheme()
|
||||
repoName := ghrepo.FullName(repo)
|
||||
opts.IO.StartProgressIndicator()
|
||||
base := fmt.Sprintf("repos/%s/actions/caches", repoName)
|
||||
|
||||
for _, cache := range toDelete {
|
||||
path := ""
|
||||
if id, err := strconv.Atoi(cache); err == nil {
|
||||
path = fmt.Sprintf("%s/%d", base, id)
|
||||
} else {
|
||||
path = fmt.Sprintf("%s?key=%s", base, cache)
|
||||
}
|
||||
|
||||
err := client.REST(repo.RepoHost(), "DELETE", path, nil, nil)
|
||||
if err != nil {
|
||||
var httpErr api.HTTPError
|
||||
if errors.As(err, &httpErr) {
|
||||
if httpErr.StatusCode == http.StatusNotFound {
|
||||
err = fmt.Errorf("%s Could not find a cache matching %s in %s", cs.FailureIcon(), cache, repoName)
|
||||
} else {
|
||||
err = fmt.Errorf("%s Failed to delete cache: %w", cs.FailureIcon(), err)
|
||||
}
|
||||
}
|
||||
opts.IO.StopProgressIndicator()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
opts.IO.StopProgressIndicator()
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
fmt.Fprintf(opts.IO.Out, "%s Deleted %s from %s\n", cs.SuccessIcon(), text.Pluralize(len(toDelete), "cache"), repoName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
209
pkg/cmd/cache/delete/delete_test.go
vendored
Normal file
209
pkg/cmd/cache/delete/delete_test.go
vendored
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
package delete
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/cache/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewCmdDelete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cli string
|
||||
wants DeleteOptions
|
||||
wantsErr string
|
||||
}{
|
||||
{
|
||||
name: "no arguments",
|
||||
cli: "",
|
||||
wantsErr: "must provide either cache id, cache key, or use --all",
|
||||
},
|
||||
{
|
||||
name: "id argument",
|
||||
cli: "123",
|
||||
wants: DeleteOptions{Identifier: "123"},
|
||||
},
|
||||
{
|
||||
name: "key argument",
|
||||
cli: "A-Cache-Key",
|
||||
wants: DeleteOptions{Identifier: "A-Cache-Key"},
|
||||
},
|
||||
{
|
||||
name: "delete all flag",
|
||||
cli: "--all",
|
||||
wants: DeleteOptions{DeleteAll: true},
|
||||
},
|
||||
{
|
||||
name: "id argument and delete all flag",
|
||||
cli: "1 --all",
|
||||
wantsErr: "specify only one of cache id, cache key, or --all",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f := &cmdutil.Factory{}
|
||||
argv, err := shlex.Split(tt.cli)
|
||||
assert.NoError(t, err)
|
||||
var gotOpts *DeleteOptions
|
||||
cmd := NewCmdDelete(f, func(opts *DeleteOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
cmd.SetArgs(argv)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(&bytes.Buffer{})
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr != "" {
|
||||
assert.EqualError(t, err, tt.wantsErr)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wants.DeleteAll, gotOpts.DeleteAll)
|
||||
assert.Equal(t, tt.wants.Identifier, gotOpts.Identifier)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts DeleteOptions
|
||||
stubs func(*httpmock.Registry)
|
||||
tty bool
|
||||
wantErr bool
|
||||
wantErrMsg string
|
||||
wantStderr string
|
||||
wantStdout string
|
||||
}{
|
||||
{
|
||||
name: "deletes cache tty",
|
||||
opts: DeleteOptions{Identifier: "123"},
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/123"),
|
||||
httpmock.StatusStringResponse(204, ""),
|
||||
)
|
||||
},
|
||||
tty: true,
|
||||
wantStdout: "✓ Deleted 1 cache from OWNER/REPO\n",
|
||||
},
|
||||
{
|
||||
name: "deletes cache notty",
|
||||
opts: DeleteOptions{Identifier: "123"},
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/123"),
|
||||
httpmock.StatusStringResponse(204, ""),
|
||||
)
|
||||
},
|
||||
tty: false,
|
||||
wantStdout: "",
|
||||
},
|
||||
{
|
||||
name: "non-existent cache",
|
||||
opts: DeleteOptions{Identifier: "123"},
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/123"),
|
||||
httpmock.StatusStringResponse(404, ""),
|
||||
)
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "X Could not find a cache matching 123 in OWNER/REPO",
|
||||
},
|
||||
{
|
||||
name: "deletes all caches",
|
||||
opts: DeleteOptions{DeleteAll: true},
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
|
||||
httpmock.JSONResponse(shared.CachePayload{
|
||||
ActionsCaches: []shared.Cache{
|
||||
{
|
||||
Id: 123,
|
||||
Key: "foo",
|
||||
CreatedAt: time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
LastAccessedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
},
|
||||
{
|
||||
Id: 456,
|
||||
Key: "bar",
|
||||
CreatedAt: time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
LastAccessedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
},
|
||||
},
|
||||
TotalCount: 2,
|
||||
}),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/123"),
|
||||
httpmock.StatusStringResponse(204, ""),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/456"),
|
||||
httpmock.StatusStringResponse(204, ""),
|
||||
)
|
||||
},
|
||||
tty: true,
|
||||
wantStdout: "✓ Deleted 2 caches from OWNER/REPO\n",
|
||||
},
|
||||
{
|
||||
name: "displays delete error",
|
||||
opts: DeleteOptions{Identifier: "123"},
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("DELETE", "repos/OWNER/REPO/actions/caches/123"),
|
||||
httpmock.StatusStringResponse(500, ""),
|
||||
)
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "X Failed to delete cache: HTTP 500 (https://api.github.com/repos/OWNER/REPO/actions/caches/123)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
if tt.stubs != nil {
|
||||
tt.stubs(reg)
|
||||
}
|
||||
tt.opts.HttpClient = func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
ios.SetStdoutTTY(tt.tty)
|
||||
ios.SetStdinTTY(tt.tty)
|
||||
ios.SetStderrTTY(tt.tty)
|
||||
tt.opts.IO = ios
|
||||
tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
}
|
||||
defer reg.Verify(t)
|
||||
|
||||
err := deleteRun(&tt.opts)
|
||||
if tt.wantErr {
|
||||
if tt.wantErrMsg != "" {
|
||||
assert.EqualError(t, err, tt.wantErrMsg)
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tt.wantStdout, stdout.String())
|
||||
assert.Equal(t, tt.wantStderr, stderr.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
149
pkg/cmd/cache/list/list.go
vendored
Normal file
149
pkg/cmd/cache/list/list.go
vendored
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/tableprinter"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/cmd/cache/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type ListOptions struct {
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
HttpClient func() (*http.Client, error)
|
||||
IO *iostreams.IOStreams
|
||||
Now time.Time
|
||||
|
||||
Limit int
|
||||
Order string
|
||||
Sort string
|
||||
}
|
||||
|
||||
func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command {
|
||||
opts := ListOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List Github Actions caches",
|
||||
Example: heredoc.Doc(`
|
||||
# List caches for current repository
|
||||
$ gh cache list
|
||||
|
||||
# List caches for specific repository
|
||||
$ gh cache list --repo cli/cli
|
||||
|
||||
# List caches sorted by least recently accessed
|
||||
$ gh cache list --sort last_accessed_at --order asc
|
||||
`),
|
||||
Aliases: []string{"ls"},
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
|
||||
if opts.Limit < 1 {
|
||||
return cmdutil.FlagErrorf("invalid limit: %v", opts.Limit)
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
return runF(&opts)
|
||||
}
|
||||
|
||||
return listRun(&opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().IntVarP(&opts.Limit, "limit", "L", 30, "Maximum number of caches to fetch")
|
||||
cmdutil.StringEnumFlag(cmd, &opts.Order, "order", "O", "desc", []string{"asc", "desc"}, "Order of caches returned")
|
||||
cmdutil.StringEnumFlag(cmd, &opts.Sort, "sort", "S", "last_accessed_at", []string{"created_at", "last_accessed_at", "size_in_bytes"}, "Sort fetched caches")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func listRun(opts *ListOptions) error {
|
||||
repo, err := opts.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
opts.IO.StartProgressIndicator()
|
||||
result, err := shared.GetCaches(client, repo, shared.GetCachesOptions{Limit: opts.Limit, Sort: opts.Sort, Order: opts.Order})
|
||||
opts.IO.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s Failed to get caches: %w", opts.IO.ColorScheme().FailureIcon(), err)
|
||||
}
|
||||
|
||||
if len(result.ActionsCaches) == 0 {
|
||||
return cmdutil.NewNoResultsError(fmt.Sprintf("No caches found in %s", ghrepo.FullName(repo)))
|
||||
}
|
||||
|
||||
if err := opts.IO.StartPager(); err == nil {
|
||||
defer opts.IO.StopPager()
|
||||
} else {
|
||||
fmt.Fprintf(opts.IO.Out, "Failed to start pager: %v\n", err)
|
||||
}
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
fmt.Fprintf(opts.IO.Out, "\nShowing %d of %s in %s\n\n", len(result.ActionsCaches), text.Pluralize(result.TotalCount, "cache"), ghrepo.FullName(repo))
|
||||
}
|
||||
|
||||
if opts.Now.IsZero() {
|
||||
opts.Now = time.Now()
|
||||
}
|
||||
|
||||
tp := tableprinter.New(opts.IO)
|
||||
tp.HeaderRow("ID", "KEY", "SIZE", "CREATED", "ACCESSED")
|
||||
for _, cache := range result.ActionsCaches {
|
||||
tp.AddField(opts.IO.ColorScheme().Cyan(fmt.Sprintf("%d", cache.Id)))
|
||||
tp.AddField(cache.Key)
|
||||
tp.AddField(humanFileSize(cache.SizeInBytes))
|
||||
tp.AddTimeField(time.Now(), cache.CreatedAt, opts.IO.ColorScheme().Gray)
|
||||
tp.AddTimeField(time.Now(), cache.LastAccessedAt, opts.IO.ColorScheme().Gray)
|
||||
tp.EndRow()
|
||||
}
|
||||
|
||||
return tp.Render()
|
||||
}
|
||||
|
||||
func humanFileSize(s int64) string {
|
||||
if s < 1024 {
|
||||
return fmt.Sprintf("%d B", s)
|
||||
}
|
||||
|
||||
kb := float64(s) / 1024
|
||||
if kb < 1024 {
|
||||
return fmt.Sprintf("%s KiB", floatToString(kb, 2))
|
||||
}
|
||||
|
||||
mb := kb / 1024
|
||||
if mb < 1024 {
|
||||
return fmt.Sprintf("%s MiB", floatToString(mb, 2))
|
||||
}
|
||||
|
||||
gb := mb / 1024
|
||||
return fmt.Sprintf("%s GiB", floatToString(gb, 2))
|
||||
}
|
||||
|
||||
func floatToString(f float64, p uint8) string {
|
||||
fs := fmt.Sprintf("%#f%0*s", f, p, "")
|
||||
idx := strings.IndexRune(fs, '.')
|
||||
return fs[:idx+int(p)+1]
|
||||
}
|
||||
296
pkg/cmd/cache/list/list_test.go
vendored
Normal file
296
pkg/cmd/cache/list/list_test.go
vendored
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/cache/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewCmdList(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wants ListOptions
|
||||
wantsErr string
|
||||
}{
|
||||
{
|
||||
name: "no arguments",
|
||||
input: "",
|
||||
wants: ListOptions{
|
||||
Limit: 30,
|
||||
Order: "desc",
|
||||
Sort: "last_accessed_at",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with limit",
|
||||
input: "--limit 100",
|
||||
wants: ListOptions{
|
||||
Limit: 100,
|
||||
Order: "desc",
|
||||
Sort: "last_accessed_at",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid limit",
|
||||
input: "-L 0",
|
||||
wantsErr: "invalid limit: 0",
|
||||
},
|
||||
{
|
||||
name: "with sort",
|
||||
input: "--sort created_at",
|
||||
wants: ListOptions{
|
||||
Limit: 30,
|
||||
Order: "desc",
|
||||
Sort: "created_at",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with order",
|
||||
input: "--order asc",
|
||||
wants: ListOptions{
|
||||
Limit: 30,
|
||||
Order: "asc",
|
||||
Sort: "last_accessed_at",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f := &cmdutil.Factory{}
|
||||
argv, err := shlex.Split(tt.input)
|
||||
assert.NoError(t, err)
|
||||
var gotOpts *ListOptions
|
||||
cmd := NewCmdList(f, func(opts *ListOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
cmd.SetArgs(argv)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(&bytes.Buffer{})
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr != "" {
|
||||
assert.EqualError(t, err, tt.wantsErr)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wants.Limit, gotOpts.Limit)
|
||||
assert.Equal(t, tt.wants.Sort, gotOpts.Sort)
|
||||
assert.Equal(t, tt.wants.Order, gotOpts.Order)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRun(t *testing.T) {
|
||||
var now = time.Date(2023, 1, 1, 1, 1, 1, 1, time.UTC)
|
||||
tests := []struct {
|
||||
name string
|
||||
opts ListOptions
|
||||
stubs func(*httpmock.Registry)
|
||||
tty bool
|
||||
wantErr bool
|
||||
wantErrMsg string
|
||||
wantStderr string
|
||||
wantStdout string
|
||||
}{
|
||||
{
|
||||
name: "displays results tty",
|
||||
tty: true,
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
|
||||
httpmock.JSONResponse(shared.CachePayload{
|
||||
ActionsCaches: []shared.Cache{
|
||||
{
|
||||
Id: 1,
|
||||
Key: "foo",
|
||||
CreatedAt: time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
LastAccessedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
SizeInBytes: 100,
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
Key: "bar",
|
||||
CreatedAt: time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
LastAccessedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
SizeInBytes: 1024,
|
||||
},
|
||||
},
|
||||
TotalCount: 2,
|
||||
}),
|
||||
)
|
||||
},
|
||||
wantStdout: heredoc.Doc(`
|
||||
|
||||
Showing 2 of 2 caches in OWNER/REPO
|
||||
|
||||
ID KEY SIZE CREATED ACCESSED
|
||||
1 foo 100 B about 2 years ago about 1 year ago
|
||||
2 bar 1.00 KiB about 2 years ago about 1 year ago
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "displays results non-tty",
|
||||
tty: false,
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
|
||||
httpmock.JSONResponse(shared.CachePayload{
|
||||
ActionsCaches: []shared.Cache{
|
||||
{
|
||||
Id: 1,
|
||||
Key: "foo",
|
||||
CreatedAt: time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
LastAccessedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
SizeInBytes: 100,
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
Key: "bar",
|
||||
CreatedAt: time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
LastAccessedAt: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
|
||||
SizeInBytes: 1024,
|
||||
},
|
||||
},
|
||||
TotalCount: 2,
|
||||
}),
|
||||
)
|
||||
},
|
||||
wantStdout: "1\tfoo\t100 B\t2021-01-01T01:01:01Z\t2022-01-01T01:01:01Z\n2\tbar\t1.00 KiB\t2021-01-01T01:01:01Z\t2022-01-01T01:01:01Z\n",
|
||||
},
|
||||
{
|
||||
name: "displays no results",
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
|
||||
httpmock.JSONResponse(shared.CachePayload{
|
||||
ActionsCaches: []shared.Cache{},
|
||||
TotalCount: 0,
|
||||
}),
|
||||
)
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "No caches found in OWNER/REPO",
|
||||
},
|
||||
{
|
||||
name: "displays list error",
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
|
||||
httpmock.StatusStringResponse(404, "Not Found"),
|
||||
)
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "X Failed to get caches: HTTP 404 (https://api.github.com/repos/OWNER/REPO/actions/caches?per_page=100)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
if tt.stubs != nil {
|
||||
tt.stubs(reg)
|
||||
}
|
||||
tt.opts.HttpClient = func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
ios.SetStdoutTTY(tt.tty)
|
||||
ios.SetStdinTTY(tt.tty)
|
||||
ios.SetStderrTTY(tt.tty)
|
||||
tt.opts.IO = ios
|
||||
tt.opts.Now = now
|
||||
tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
}
|
||||
defer reg.Verify(t)
|
||||
|
||||
err := listRun(&tt.opts)
|
||||
if tt.wantErr {
|
||||
if tt.wantErrMsg != "" {
|
||||
assert.EqualError(t, err, tt.wantErrMsg)
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tt.wantStdout, stdout.String())
|
||||
assert.Equal(t, tt.wantStderr, stderr.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_humanFileSize(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
size int64
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "min bytes",
|
||||
size: 1,
|
||||
want: "1 B",
|
||||
},
|
||||
{
|
||||
name: "max bytes",
|
||||
size: 1023,
|
||||
want: "1023 B",
|
||||
},
|
||||
{
|
||||
name: "min kibibytes",
|
||||
size: 1024,
|
||||
want: "1.00 KiB",
|
||||
},
|
||||
{
|
||||
name: "max kibibytes",
|
||||
size: 1024*1024 - 1,
|
||||
want: "1023.99 KiB",
|
||||
},
|
||||
{
|
||||
name: "min mibibytes",
|
||||
size: 1024 * 1024,
|
||||
want: "1.00 MiB",
|
||||
},
|
||||
{
|
||||
name: "fractional mibibytes",
|
||||
size: 1024*1024*12 + 1024*350,
|
||||
want: "12.34 MiB",
|
||||
},
|
||||
{
|
||||
name: "max mibibytes",
|
||||
size: 1024*1024*1024 - 1,
|
||||
want: "1023.99 MiB",
|
||||
},
|
||||
{
|
||||
name: "min gibibytes",
|
||||
size: 1024 * 1024 * 1024,
|
||||
want: "1.00 GiB",
|
||||
},
|
||||
{
|
||||
name: "fractional gibibytes",
|
||||
size: 1024 * 1024 * 1024 * 1.5,
|
||||
want: "1.50 GiB",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := humanFileSize(tt.size); got != tt.want {
|
||||
t.Errorf("humanFileSize() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
73
pkg/cmd/cache/shared/shared.go
vendored
Normal file
73
pkg/cmd/cache/shared/shared.go
vendored
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
)
|
||||
|
||||
type Cache struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Id int `json:"id"`
|
||||
Key string `json:"key"`
|
||||
LastAccessedAt time.Time `json:"last_accessed_at"`
|
||||
Ref string `json:"ref"`
|
||||
SizeInBytes int64 `json:"size_in_bytes"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type CachePayload struct {
|
||||
ActionsCaches []Cache `json:"actions_caches"`
|
||||
TotalCount int `json:"total_count"`
|
||||
}
|
||||
|
||||
type GetCachesOptions struct {
|
||||
Limit int
|
||||
Order string
|
||||
Sort string
|
||||
}
|
||||
|
||||
// Return a list of caches for a repository. Pass a negative limit to request
|
||||
// all pages from the API until all caches have been fetched.
|
||||
func GetCaches(client *api.Client, repo ghrepo.Interface, opts GetCachesOptions) (*CachePayload, error) {
|
||||
path := fmt.Sprintf("repos/%s/actions/caches", ghrepo.FullName(repo))
|
||||
|
||||
perPage := 100
|
||||
if opts.Limit > 0 && opts.Limit < 100 {
|
||||
perPage = opts.Limit
|
||||
}
|
||||
path += fmt.Sprintf("?per_page=%d", perPage)
|
||||
|
||||
if opts.Sort != "" {
|
||||
path += fmt.Sprintf("&sort=%s", opts.Sort)
|
||||
}
|
||||
if opts.Order != "" {
|
||||
path += fmt.Sprintf("&direction=%s", opts.Order)
|
||||
}
|
||||
|
||||
var result *CachePayload
|
||||
pagination:
|
||||
for path != "" {
|
||||
var response CachePayload
|
||||
var err error
|
||||
path, err = client.RESTWithNext(repo.RepoHost(), "GET", path, nil, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
result = &response
|
||||
} else {
|
||||
result.ActionsCaches = append(result.ActionsCaches, response.ActionsCaches...)
|
||||
}
|
||||
|
||||
if opts.Limit > 0 && len(result.ActionsCaches) >= opts.Limit {
|
||||
result.ActionsCaches = result.ActionsCaches[:opts.Limit]
|
||||
break pagination
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
68
pkg/cmd/cache/shared/shared_test.go
vendored
Normal file
68
pkg/cmd/cache/shared/shared_test.go
vendored
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetCaches(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts GetCachesOptions
|
||||
stubs func(*httpmock.Registry)
|
||||
wantsCount int
|
||||
}{
|
||||
{
|
||||
name: "no caches",
|
||||
opts: GetCachesOptions{},
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
|
||||
httpmock.StringResponse(`{"actions_caches": [], "total_count": 0}`),
|
||||
)
|
||||
},
|
||||
wantsCount: 0,
|
||||
},
|
||||
{
|
||||
name: "limits cache count",
|
||||
opts: GetCachesOptions{Limit: 1},
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
|
||||
httpmock.StringResponse(`{"actions_caches": [{"id": 1}, {"id": 2}], "total_count": 2}`),
|
||||
)
|
||||
},
|
||||
wantsCount: 1,
|
||||
},
|
||||
{
|
||||
name: "negative limit returns all caches",
|
||||
opts: GetCachesOptions{Limit: -1},
|
||||
stubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"),
|
||||
httpmock.StringResponse(`{"actions_caches": [{"id": 1}, {"id": 2}], "total_count": 2}`),
|
||||
)
|
||||
},
|
||||
wantsCount: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
tt.stubs(reg)
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
client := api.NewClientFromHTTP(httpClient)
|
||||
repo, err := ghrepo.FromFullName("OWNER/REPO")
|
||||
assert.NoError(t, err)
|
||||
result, err := GetCaches(client, repo, tt.opts)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantsCount, len(result.ActionsCaches))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -88,6 +88,7 @@ func startLiveShareSession(ctx context.Context, codespace *api.Codespace, a *App
|
|||
|
||||
//go:generate moq -fmt goimports -rm -skip-ensure -out mock_api.go . apiClient
|
||||
type apiClient interface {
|
||||
ServerURL() string
|
||||
GetUser(ctx context.Context) (*api.User, error)
|
||||
GetCodespace(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error)
|
||||
GetOrgMemberCodespace(ctx context.Context, orgName string, userName string, codespaceName string) (*api.Codespace, error)
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ func (a *App) Create(ctx context.Context, opts createOptions) error {
|
|||
}
|
||||
|
||||
if opts.useWeb && userInputs.Repository == "" {
|
||||
return a.browser.Browse("https://github.com/codespaces/new")
|
||||
return a.browser.Browse(fmt.Sprintf("%s/codespaces/new", a.apiClient.ServerURL()))
|
||||
}
|
||||
|
||||
promptForRepoAndBranch := userInputs.Repository == "" && !opts.useWeb
|
||||
|
|
@ -293,7 +293,7 @@ func (a *App) Create(ctx context.Context, opts createOptions) error {
|
|||
}
|
||||
|
||||
if opts.useWeb {
|
||||
return a.browser.Browse(fmt.Sprintf("https://github.com/codespaces/new?repo=%d&ref=%s&machine=%s&location=%s", createParams.RepositoryID, createParams.Branch, createParams.Machine, createParams.Location))
|
||||
return a.browser.Browse(fmt.Sprintf("%s/codespaces/new?repo=%d&ref=%s&machine=%s&location=%s", a.apiClient.ServerURL(), createParams.RepositoryID, createParams.Branch, createParams.Machine, createParams.Location))
|
||||
}
|
||||
|
||||
var codespace *api.Codespace
|
||||
|
|
|
|||
|
|
@ -453,11 +453,32 @@ Alternatively, you can run "create" with the "--default-permissions" option to c
|
|||
},
|
||||
{
|
||||
name: "return default url when using web flag without other flags",
|
||||
fields: fields{
|
||||
apiClient: apiCreateDefaults(&apiClientMock{
|
||||
ServerURLFunc: func() string {
|
||||
return "https://github.com"
|
||||
},
|
||||
}),
|
||||
},
|
||||
opts: createOptions{
|
||||
useWeb: true,
|
||||
},
|
||||
wantURL: "https://github.com/codespaces/new",
|
||||
},
|
||||
{
|
||||
name: "return custom server url when using web flag",
|
||||
fields: fields{
|
||||
apiClient: apiCreateDefaults(&apiClientMock{
|
||||
ServerURLFunc: func() string {
|
||||
return "https://github.mycompany.com"
|
||||
},
|
||||
}),
|
||||
},
|
||||
opts: createOptions{
|
||||
useWeb: true,
|
||||
},
|
||||
wantURL: "https://github.mycompany.com/codespaces/new",
|
||||
},
|
||||
{
|
||||
name: "skip machine check when using web flag and no machine provided",
|
||||
fields: fields{
|
||||
|
|
@ -473,6 +494,9 @@ Alternatively, you can run "create" with the "--default-permissions" option to c
|
|||
Name: "monalisa-dotfiles-abcd1234",
|
||||
}, nil
|
||||
},
|
||||
ServerURLFunc: func() string {
|
||||
return "https://github.com"
|
||||
},
|
||||
}),
|
||||
},
|
||||
opts: createOptions{
|
||||
|
|
@ -499,6 +523,9 @@ Alternatively, you can run "create" with the "--default-permissions" option to c
|
|||
Name: "monalisa-dotfiles-abcd1234",
|
||||
}, nil
|
||||
},
|
||||
ServerURLFunc: func() string {
|
||||
return "https://github.com"
|
||||
},
|
||||
}),
|
||||
},
|
||||
opts: createOptions{
|
||||
|
|
@ -524,6 +551,9 @@ Alternatively, you can run "create" with the "--default-permissions" option to c
|
|||
Machine: api.CodespaceMachine{Name: "GIGA"},
|
||||
}, nil
|
||||
},
|
||||
ServerURLFunc: func() string {
|
||||
return "https://github.com"
|
||||
},
|
||||
}),
|
||||
},
|
||||
opts: createOptions{
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ func newListCmd(app *App) *cobra.Command {
|
|||
|
||||
func (a *App) List(ctx context.Context, opts *listOptions, exporter cmdutil.Exporter) error {
|
||||
if opts.useWeb && opts.repo == "" {
|
||||
return a.browser.Browse("https://github.com/codespaces")
|
||||
return a.browser.Browse(fmt.Sprintf("%s/codespaces", a.apiClient.ServerURL()))
|
||||
}
|
||||
|
||||
var codespaces []*api.Codespace
|
||||
|
|
@ -113,7 +113,7 @@ func (a *App) List(ctx context.Context, opts *listOptions, exporter cmdutil.Expo
|
|||
}
|
||||
|
||||
if opts.useWeb && codespaces[0].Repository.ID > 0 {
|
||||
return a.browser.Browse(fmt.Sprintf("https://github.com/codespaces?repository_id=%d", codespaces[0].Repository.ID))
|
||||
return a.browser.Browse(fmt.Sprintf("%s/codespaces?repository_id=%d", a.apiClient.ServerURL(), codespaces[0].Repository.ID))
|
||||
}
|
||||
|
||||
//nolint:staticcheck // SA1019: utils.NewTablePrinter is deprecated: use internal/tableprinter
|
||||
|
|
|
|||
|
|
@ -170,11 +170,32 @@ func TestApp_List(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "list codespaces,--web",
|
||||
fields: fields{
|
||||
apiClient: &apiClientMock{
|
||||
ServerURLFunc: func() string {
|
||||
return "https://github.com"
|
||||
},
|
||||
},
|
||||
},
|
||||
opts: &listOptions{
|
||||
useWeb: true,
|
||||
},
|
||||
wantURL: "https://github.com/codespaces",
|
||||
},
|
||||
{
|
||||
name: "list codespaces,--web with custom server url",
|
||||
fields: fields{
|
||||
apiClient: &apiClientMock{
|
||||
ServerURLFunc: func() string {
|
||||
return "https://github.mycompany.com"
|
||||
},
|
||||
},
|
||||
},
|
||||
opts: &listOptions{
|
||||
useWeb: true,
|
||||
},
|
||||
wantURL: "https://github.mycompany.com/codespaces",
|
||||
},
|
||||
{
|
||||
name: "list codespaces,--web, --repo flag",
|
||||
fields: fields{
|
||||
|
|
@ -199,6 +220,9 @@ func TestApp_List(t *testing.T) {
|
|||
},
|
||||
}, nil
|
||||
},
|
||||
ServerURLFunc: func() string {
|
||||
return "https://github.com"
|
||||
},
|
||||
},
|
||||
},
|
||||
opts: &listOptions{
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/cli/cli/v2/internal/codespaces/api"
|
||||
codespacesAPI "github.com/cli/cli/v2/internal/codespaces/api"
|
||||
)
|
||||
|
||||
// apiClientMock is a mock implementation of apiClient.
|
||||
|
|
@ -16,43 +16,46 @@ import (
|
|||
//
|
||||
// // make and configure a mocked apiClient
|
||||
// mockedapiClient := &apiClientMock{
|
||||
// CreateCodespaceFunc: func(ctx context.Context, params *api.CreateCodespaceParams) (*api.Codespace, error) {
|
||||
// CreateCodespaceFunc: func(ctx context.Context, params *codespacesAPI.CreateCodespaceParams) (*codespacesAPI.Codespace, error) {
|
||||
// panic("mock out the CreateCodespace method")
|
||||
// },
|
||||
// DeleteCodespaceFunc: func(ctx context.Context, name string, orgName string, userName string) error {
|
||||
// panic("mock out the DeleteCodespace method")
|
||||
// },
|
||||
// EditCodespaceFunc: func(ctx context.Context, codespaceName string, params *api.EditCodespaceParams) (*api.Codespace, error) {
|
||||
// EditCodespaceFunc: func(ctx context.Context, codespaceName string, params *codespacesAPI.EditCodespaceParams) (*codespacesAPI.Codespace, error) {
|
||||
// panic("mock out the EditCodespace method")
|
||||
// },
|
||||
// GetCodespaceFunc: func(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error) {
|
||||
// GetCodespaceFunc: func(ctx context.Context, name string, includeConnection bool) (*codespacesAPI.Codespace, error) {
|
||||
// panic("mock out the GetCodespace method")
|
||||
// },
|
||||
// GetCodespaceBillableOwnerFunc: func(ctx context.Context, nwo string) (*api.User, error) {
|
||||
// GetCodespaceBillableOwnerFunc: func(ctx context.Context, nwo string) (*codespacesAPI.User, error) {
|
||||
// panic("mock out the GetCodespaceBillableOwner method")
|
||||
// },
|
||||
// GetCodespaceRepoSuggestionsFunc: func(ctx context.Context, partialSearch string, params api.RepoSearchParameters) ([]string, error) {
|
||||
// GetCodespaceRepoSuggestionsFunc: func(ctx context.Context, partialSearch string, params codespacesAPI.RepoSearchParameters) ([]string, error) {
|
||||
// panic("mock out the GetCodespaceRepoSuggestions method")
|
||||
// },
|
||||
// GetCodespaceRepositoryContentsFunc: func(ctx context.Context, codespace *api.Codespace, path string) ([]byte, error) {
|
||||
// GetCodespaceRepositoryContentsFunc: func(ctx context.Context, codespace *codespacesAPI.Codespace, path string) ([]byte, error) {
|
||||
// panic("mock out the GetCodespaceRepositoryContents method")
|
||||
// },
|
||||
// GetCodespacesMachinesFunc: func(ctx context.Context, repoID int, branch string, location string, devcontainerPath string) ([]*api.Machine, error) {
|
||||
// GetCodespacesMachinesFunc: func(ctx context.Context, repoID int, branch string, location string, devcontainerPath string) ([]*codespacesAPI.Machine, error) {
|
||||
// panic("mock out the GetCodespacesMachines method")
|
||||
// },
|
||||
// GetOrgMemberCodespaceFunc: func(ctx context.Context, orgName string, userName string, codespaceName string) (*api.Codespace, error) {
|
||||
// GetOrgMemberCodespaceFunc: func(ctx context.Context, orgName string, userName string, codespaceName string) (*codespacesAPI.Codespace, error) {
|
||||
// panic("mock out the GetOrgMemberCodespace method")
|
||||
// },
|
||||
// GetRepositoryFunc: func(ctx context.Context, nwo string) (*api.Repository, error) {
|
||||
// GetRepositoryFunc: func(ctx context.Context, nwo string) (*codespacesAPI.Repository, error) {
|
||||
// panic("mock out the GetRepository method")
|
||||
// },
|
||||
// GetUserFunc: func(ctx context.Context) (*api.User, error) {
|
||||
// ServerURLFunc: func() string {
|
||||
// panic("mock out the ServerURL method")
|
||||
// },
|
||||
// GetUserFunc: func(ctx context.Context) (*codespacesAPI.User, error) {
|
||||
// panic("mock out the GetUser method")
|
||||
// },
|
||||
// ListCodespacesFunc: func(ctx context.Context, opts api.ListCodespacesOptions) ([]*api.Codespace, error) {
|
||||
// ListCodespacesFunc: func(ctx context.Context, opts codespacesAPI.ListCodespacesOptions) ([]*codespacesAPI.Codespace, error) {
|
||||
// panic("mock out the ListCodespaces method")
|
||||
// },
|
||||
// ListDevContainersFunc: func(ctx context.Context, repoID int, branch string, limit int) ([]api.DevContainerEntry, error) {
|
||||
// ListDevContainersFunc: func(ctx context.Context, repoID int, branch string, limit int) ([]codespacesAPI.DevContainerEntry, error) {
|
||||
// panic("mock out the ListDevContainers method")
|
||||
// },
|
||||
// StartCodespaceFunc: func(ctx context.Context, name string) error {
|
||||
|
|
@ -69,43 +72,46 @@ import (
|
|||
// }
|
||||
type apiClientMock struct {
|
||||
// CreateCodespaceFunc mocks the CreateCodespace method.
|
||||
CreateCodespaceFunc func(ctx context.Context, params *api.CreateCodespaceParams) (*api.Codespace, error)
|
||||
CreateCodespaceFunc func(ctx context.Context, params *codespacesAPI.CreateCodespaceParams) (*codespacesAPI.Codespace, error)
|
||||
|
||||
// DeleteCodespaceFunc mocks the DeleteCodespace method.
|
||||
DeleteCodespaceFunc func(ctx context.Context, name string, orgName string, userName string) error
|
||||
|
||||
// EditCodespaceFunc mocks the EditCodespace method.
|
||||
EditCodespaceFunc func(ctx context.Context, codespaceName string, params *api.EditCodespaceParams) (*api.Codespace, error)
|
||||
EditCodespaceFunc func(ctx context.Context, codespaceName string, params *codespacesAPI.EditCodespaceParams) (*codespacesAPI.Codespace, error)
|
||||
|
||||
// GetCodespaceFunc mocks the GetCodespace method.
|
||||
GetCodespaceFunc func(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error)
|
||||
GetCodespaceFunc func(ctx context.Context, name string, includeConnection bool) (*codespacesAPI.Codespace, error)
|
||||
|
||||
// GetCodespaceBillableOwnerFunc mocks the GetCodespaceBillableOwner method.
|
||||
GetCodespaceBillableOwnerFunc func(ctx context.Context, nwo string) (*api.User, error)
|
||||
GetCodespaceBillableOwnerFunc func(ctx context.Context, nwo string) (*codespacesAPI.User, error)
|
||||
|
||||
// GetCodespaceRepoSuggestionsFunc mocks the GetCodespaceRepoSuggestions method.
|
||||
GetCodespaceRepoSuggestionsFunc func(ctx context.Context, partialSearch string, params api.RepoSearchParameters) ([]string, error)
|
||||
GetCodespaceRepoSuggestionsFunc func(ctx context.Context, partialSearch string, params codespacesAPI.RepoSearchParameters) ([]string, error)
|
||||
|
||||
// GetCodespaceRepositoryContentsFunc mocks the GetCodespaceRepositoryContents method.
|
||||
GetCodespaceRepositoryContentsFunc func(ctx context.Context, codespace *api.Codespace, path string) ([]byte, error)
|
||||
GetCodespaceRepositoryContentsFunc func(ctx context.Context, codespace *codespacesAPI.Codespace, path string) ([]byte, error)
|
||||
|
||||
// GetCodespacesMachinesFunc mocks the GetCodespacesMachines method.
|
||||
GetCodespacesMachinesFunc func(ctx context.Context, repoID int, branch string, location string, devcontainerPath string) ([]*api.Machine, error)
|
||||
GetCodespacesMachinesFunc func(ctx context.Context, repoID int, branch string, location string, devcontainerPath string) ([]*codespacesAPI.Machine, error)
|
||||
|
||||
// GetOrgMemberCodespaceFunc mocks the GetOrgMemberCodespace method.
|
||||
GetOrgMemberCodespaceFunc func(ctx context.Context, orgName string, userName string, codespaceName string) (*api.Codespace, error)
|
||||
GetOrgMemberCodespaceFunc func(ctx context.Context, orgName string, userName string, codespaceName string) (*codespacesAPI.Codespace, error)
|
||||
|
||||
// GetRepositoryFunc mocks the GetRepository method.
|
||||
GetRepositoryFunc func(ctx context.Context, nwo string) (*api.Repository, error)
|
||||
GetRepositoryFunc func(ctx context.Context, nwo string) (*codespacesAPI.Repository, error)
|
||||
|
||||
// ServerURLFunc mocks the ServerURL method.
|
||||
ServerURLFunc func() string
|
||||
|
||||
// GetUserFunc mocks the GetUser method.
|
||||
GetUserFunc func(ctx context.Context) (*api.User, error)
|
||||
GetUserFunc func(ctx context.Context) (*codespacesAPI.User, error)
|
||||
|
||||
// ListCodespacesFunc mocks the ListCodespaces method.
|
||||
ListCodespacesFunc func(ctx context.Context, opts api.ListCodespacesOptions) ([]*api.Codespace, error)
|
||||
ListCodespacesFunc func(ctx context.Context, opts codespacesAPI.ListCodespacesOptions) ([]*codespacesAPI.Codespace, error)
|
||||
|
||||
// ListDevContainersFunc mocks the ListDevContainers method.
|
||||
ListDevContainersFunc func(ctx context.Context, repoID int, branch string, limit int) ([]api.DevContainerEntry, error)
|
||||
ListDevContainersFunc func(ctx context.Context, repoID int, branch string, limit int) ([]codespacesAPI.DevContainerEntry, error)
|
||||
|
||||
// StartCodespaceFunc mocks the StartCodespace method.
|
||||
StartCodespaceFunc func(ctx context.Context, name string) error
|
||||
|
|
@ -120,7 +126,7 @@ type apiClientMock struct {
|
|||
// Ctx is the ctx argument value.
|
||||
Ctx context.Context
|
||||
// Params is the params argument value.
|
||||
Params *api.CreateCodespaceParams
|
||||
Params *codespacesAPI.CreateCodespaceParams
|
||||
}
|
||||
// DeleteCodespace holds details about calls to the DeleteCodespace method.
|
||||
DeleteCodespace []struct {
|
||||
|
|
@ -140,7 +146,7 @@ type apiClientMock struct {
|
|||
// CodespaceName is the codespaceName argument value.
|
||||
CodespaceName string
|
||||
// Params is the params argument value.
|
||||
Params *api.EditCodespaceParams
|
||||
Params *codespacesAPI.EditCodespaceParams
|
||||
}
|
||||
// GetCodespace holds details about calls to the GetCodespace method.
|
||||
GetCodespace []struct {
|
||||
|
|
@ -165,14 +171,14 @@ type apiClientMock struct {
|
|||
// PartialSearch is the partialSearch argument value.
|
||||
PartialSearch string
|
||||
// Params is the params argument value.
|
||||
Params api.RepoSearchParameters
|
||||
Params codespacesAPI.RepoSearchParameters
|
||||
}
|
||||
// GetCodespaceRepositoryContents holds details about calls to the GetCodespaceRepositoryContents method.
|
||||
GetCodespaceRepositoryContents []struct {
|
||||
// Ctx is the ctx argument value.
|
||||
Ctx context.Context
|
||||
// Codespace is the codespace argument value.
|
||||
Codespace *api.Codespace
|
||||
Codespace *codespacesAPI.Codespace
|
||||
// Path is the path argument value.
|
||||
Path string
|
||||
}
|
||||
|
|
@ -207,6 +213,9 @@ type apiClientMock struct {
|
|||
// Nwo is the nwo argument value.
|
||||
Nwo string
|
||||
}
|
||||
// ServerURL holds details about calls to the ServerURL method.
|
||||
ServerURL []struct {
|
||||
}
|
||||
// GetUser holds details about calls to the GetUser method.
|
||||
GetUser []struct {
|
||||
// Ctx is the ctx argument value.
|
||||
|
|
@ -217,7 +226,7 @@ type apiClientMock struct {
|
|||
// Ctx is the ctx argument value.
|
||||
Ctx context.Context
|
||||
// Opts is the opts argument value.
|
||||
Opts api.ListCodespacesOptions
|
||||
Opts codespacesAPI.ListCodespacesOptions
|
||||
}
|
||||
// ListDevContainers holds details about calls to the ListDevContainers method.
|
||||
ListDevContainers []struct {
|
||||
|
|
@ -259,6 +268,7 @@ type apiClientMock struct {
|
|||
lockGetCodespacesMachines sync.RWMutex
|
||||
lockGetOrgMemberCodespace sync.RWMutex
|
||||
lockGetRepository sync.RWMutex
|
||||
lockServerURL sync.RWMutex
|
||||
lockGetUser sync.RWMutex
|
||||
lockListCodespaces sync.RWMutex
|
||||
lockListDevContainers sync.RWMutex
|
||||
|
|
@ -267,13 +277,13 @@ type apiClientMock struct {
|
|||
}
|
||||
|
||||
// CreateCodespace calls CreateCodespaceFunc.
|
||||
func (mock *apiClientMock) CreateCodespace(ctx context.Context, params *api.CreateCodespaceParams) (*api.Codespace, error) {
|
||||
func (mock *apiClientMock) CreateCodespace(ctx context.Context, params *codespacesAPI.CreateCodespaceParams) (*codespacesAPI.Codespace, error) {
|
||||
if mock.CreateCodespaceFunc == nil {
|
||||
panic("apiClientMock.CreateCodespaceFunc: method is nil but apiClient.CreateCodespace was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
Params *api.CreateCodespaceParams
|
||||
Params *codespacesAPI.CreateCodespaceParams
|
||||
}{
|
||||
Ctx: ctx,
|
||||
Params: params,
|
||||
|
|
@ -290,11 +300,11 @@ func (mock *apiClientMock) CreateCodespace(ctx context.Context, params *api.Crea
|
|||
// len(mockedapiClient.CreateCodespaceCalls())
|
||||
func (mock *apiClientMock) CreateCodespaceCalls() []struct {
|
||||
Ctx context.Context
|
||||
Params *api.CreateCodespaceParams
|
||||
Params *codespacesAPI.CreateCodespaceParams
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
Params *api.CreateCodespaceParams
|
||||
Params *codespacesAPI.CreateCodespaceParams
|
||||
}
|
||||
mock.lockCreateCodespace.RLock()
|
||||
calls = mock.calls.CreateCodespace
|
||||
|
|
@ -347,14 +357,14 @@ func (mock *apiClientMock) DeleteCodespaceCalls() []struct {
|
|||
}
|
||||
|
||||
// EditCodespace calls EditCodespaceFunc.
|
||||
func (mock *apiClientMock) EditCodespace(ctx context.Context, codespaceName string, params *api.EditCodespaceParams) (*api.Codespace, error) {
|
||||
func (mock *apiClientMock) EditCodespace(ctx context.Context, codespaceName string, params *codespacesAPI.EditCodespaceParams) (*codespacesAPI.Codespace, error) {
|
||||
if mock.EditCodespaceFunc == nil {
|
||||
panic("apiClientMock.EditCodespaceFunc: method is nil but apiClient.EditCodespace was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
CodespaceName string
|
||||
Params *api.EditCodespaceParams
|
||||
Params *codespacesAPI.EditCodespaceParams
|
||||
}{
|
||||
Ctx: ctx,
|
||||
CodespaceName: codespaceName,
|
||||
|
|
@ -373,12 +383,12 @@ func (mock *apiClientMock) EditCodespace(ctx context.Context, codespaceName stri
|
|||
func (mock *apiClientMock) EditCodespaceCalls() []struct {
|
||||
Ctx context.Context
|
||||
CodespaceName string
|
||||
Params *api.EditCodespaceParams
|
||||
Params *codespacesAPI.EditCodespaceParams
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
CodespaceName string
|
||||
Params *api.EditCodespaceParams
|
||||
Params *codespacesAPI.EditCodespaceParams
|
||||
}
|
||||
mock.lockEditCodespace.RLock()
|
||||
calls = mock.calls.EditCodespace
|
||||
|
|
@ -387,7 +397,7 @@ func (mock *apiClientMock) EditCodespaceCalls() []struct {
|
|||
}
|
||||
|
||||
// GetCodespace calls GetCodespaceFunc.
|
||||
func (mock *apiClientMock) GetCodespace(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error) {
|
||||
func (mock *apiClientMock) GetCodespace(ctx context.Context, name string, includeConnection bool) (*codespacesAPI.Codespace, error) {
|
||||
if mock.GetCodespaceFunc == nil {
|
||||
panic("apiClientMock.GetCodespaceFunc: method is nil but apiClient.GetCodespace was just called")
|
||||
}
|
||||
|
|
@ -427,7 +437,7 @@ func (mock *apiClientMock) GetCodespaceCalls() []struct {
|
|||
}
|
||||
|
||||
// GetCodespaceBillableOwner calls GetCodespaceBillableOwnerFunc.
|
||||
func (mock *apiClientMock) GetCodespaceBillableOwner(ctx context.Context, nwo string) (*api.User, error) {
|
||||
func (mock *apiClientMock) GetCodespaceBillableOwner(ctx context.Context, nwo string) (*codespacesAPI.User, error) {
|
||||
if mock.GetCodespaceBillableOwnerFunc == nil {
|
||||
panic("apiClientMock.GetCodespaceBillableOwnerFunc: method is nil but apiClient.GetCodespaceBillableOwner was just called")
|
||||
}
|
||||
|
|
@ -463,14 +473,14 @@ func (mock *apiClientMock) GetCodespaceBillableOwnerCalls() []struct {
|
|||
}
|
||||
|
||||
// GetCodespaceRepoSuggestions calls GetCodespaceRepoSuggestionsFunc.
|
||||
func (mock *apiClientMock) GetCodespaceRepoSuggestions(ctx context.Context, partialSearch string, params api.RepoSearchParameters) ([]string, error) {
|
||||
func (mock *apiClientMock) GetCodespaceRepoSuggestions(ctx context.Context, partialSearch string, params codespacesAPI.RepoSearchParameters) ([]string, error) {
|
||||
if mock.GetCodespaceRepoSuggestionsFunc == nil {
|
||||
panic("apiClientMock.GetCodespaceRepoSuggestionsFunc: method is nil but apiClient.GetCodespaceRepoSuggestions was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
PartialSearch string
|
||||
Params api.RepoSearchParameters
|
||||
Params codespacesAPI.RepoSearchParameters
|
||||
}{
|
||||
Ctx: ctx,
|
||||
PartialSearch: partialSearch,
|
||||
|
|
@ -489,12 +499,12 @@ func (mock *apiClientMock) GetCodespaceRepoSuggestions(ctx context.Context, part
|
|||
func (mock *apiClientMock) GetCodespaceRepoSuggestionsCalls() []struct {
|
||||
Ctx context.Context
|
||||
PartialSearch string
|
||||
Params api.RepoSearchParameters
|
||||
Params codespacesAPI.RepoSearchParameters
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
PartialSearch string
|
||||
Params api.RepoSearchParameters
|
||||
Params codespacesAPI.RepoSearchParameters
|
||||
}
|
||||
mock.lockGetCodespaceRepoSuggestions.RLock()
|
||||
calls = mock.calls.GetCodespaceRepoSuggestions
|
||||
|
|
@ -503,13 +513,13 @@ func (mock *apiClientMock) GetCodespaceRepoSuggestionsCalls() []struct {
|
|||
}
|
||||
|
||||
// GetCodespaceRepositoryContents calls GetCodespaceRepositoryContentsFunc.
|
||||
func (mock *apiClientMock) GetCodespaceRepositoryContents(ctx context.Context, codespace *api.Codespace, path string) ([]byte, error) {
|
||||
func (mock *apiClientMock) GetCodespaceRepositoryContents(ctx context.Context, codespace *codespacesAPI.Codespace, path string) ([]byte, error) {
|
||||
if mock.GetCodespaceRepositoryContentsFunc == nil {
|
||||
panic("apiClientMock.GetCodespaceRepositoryContentsFunc: method is nil but apiClient.GetCodespaceRepositoryContents was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
Codespace *api.Codespace
|
||||
Codespace *codespacesAPI.Codespace
|
||||
Path string
|
||||
}{
|
||||
Ctx: ctx,
|
||||
|
|
@ -528,12 +538,12 @@ func (mock *apiClientMock) GetCodespaceRepositoryContents(ctx context.Context, c
|
|||
// len(mockedapiClient.GetCodespaceRepositoryContentsCalls())
|
||||
func (mock *apiClientMock) GetCodespaceRepositoryContentsCalls() []struct {
|
||||
Ctx context.Context
|
||||
Codespace *api.Codespace
|
||||
Codespace *codespacesAPI.Codespace
|
||||
Path string
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
Codespace *api.Codespace
|
||||
Codespace *codespacesAPI.Codespace
|
||||
Path string
|
||||
}
|
||||
mock.lockGetCodespaceRepositoryContents.RLock()
|
||||
|
|
@ -543,7 +553,7 @@ func (mock *apiClientMock) GetCodespaceRepositoryContentsCalls() []struct {
|
|||
}
|
||||
|
||||
// GetCodespacesMachines calls GetCodespacesMachinesFunc.
|
||||
func (mock *apiClientMock) GetCodespacesMachines(ctx context.Context, repoID int, branch string, location string, devcontainerPath string) ([]*api.Machine, error) {
|
||||
func (mock *apiClientMock) GetCodespacesMachines(ctx context.Context, repoID int, branch string, location string, devcontainerPath string) ([]*codespacesAPI.Machine, error) {
|
||||
if mock.GetCodespacesMachinesFunc == nil {
|
||||
panic("apiClientMock.GetCodespacesMachinesFunc: method is nil but apiClient.GetCodespacesMachines was just called")
|
||||
}
|
||||
|
|
@ -591,7 +601,7 @@ func (mock *apiClientMock) GetCodespacesMachinesCalls() []struct {
|
|||
}
|
||||
|
||||
// GetOrgMemberCodespace calls GetOrgMemberCodespaceFunc.
|
||||
func (mock *apiClientMock) GetOrgMemberCodespace(ctx context.Context, orgName string, userName string, codespaceName string) (*api.Codespace, error) {
|
||||
func (mock *apiClientMock) GetOrgMemberCodespace(ctx context.Context, orgName string, userName string, codespaceName string) (*codespacesAPI.Codespace, error) {
|
||||
if mock.GetOrgMemberCodespaceFunc == nil {
|
||||
panic("apiClientMock.GetOrgMemberCodespaceFunc: method is nil but apiClient.GetOrgMemberCodespace was just called")
|
||||
}
|
||||
|
|
@ -635,7 +645,7 @@ func (mock *apiClientMock) GetOrgMemberCodespaceCalls() []struct {
|
|||
}
|
||||
|
||||
// GetRepository calls GetRepositoryFunc.
|
||||
func (mock *apiClientMock) GetRepository(ctx context.Context, nwo string) (*api.Repository, error) {
|
||||
func (mock *apiClientMock) GetRepository(ctx context.Context, nwo string) (*codespacesAPI.Repository, error) {
|
||||
if mock.GetRepositoryFunc == nil {
|
||||
panic("apiClientMock.GetRepositoryFunc: method is nil but apiClient.GetRepository was just called")
|
||||
}
|
||||
|
|
@ -670,8 +680,35 @@ func (mock *apiClientMock) GetRepositoryCalls() []struct {
|
|||
return calls
|
||||
}
|
||||
|
||||
// ServerURL calls ServerURLFunc.
|
||||
func (mock *apiClientMock) ServerURL() string {
|
||||
if mock.ServerURLFunc == nil {
|
||||
panic("apiClientMock.ServerURLFunc: method is nil but apiClient.ServerURL was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
}{}
|
||||
mock.lockServerURL.Lock()
|
||||
mock.calls.ServerURL = append(mock.calls.ServerURL, callInfo)
|
||||
mock.lockServerURL.Unlock()
|
||||
return mock.ServerURLFunc()
|
||||
}
|
||||
|
||||
// ServerURLCalls gets all the calls that were made to ServerURL.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedapiClient.ServerURLCalls())
|
||||
func (mock *apiClientMock) ServerURLCalls() []struct {
|
||||
} {
|
||||
var calls []struct {
|
||||
}
|
||||
mock.lockServerURL.RLock()
|
||||
calls = mock.calls.ServerURL
|
||||
mock.lockServerURL.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// GetUser calls GetUserFunc.
|
||||
func (mock *apiClientMock) GetUser(ctx context.Context) (*api.User, error) {
|
||||
func (mock *apiClientMock) GetUser(ctx context.Context) (*codespacesAPI.User, error) {
|
||||
if mock.GetUserFunc == nil {
|
||||
panic("apiClientMock.GetUserFunc: method is nil but apiClient.GetUser was just called")
|
||||
}
|
||||
|
|
@ -703,13 +740,13 @@ func (mock *apiClientMock) GetUserCalls() []struct {
|
|||
}
|
||||
|
||||
// ListCodespaces calls ListCodespacesFunc.
|
||||
func (mock *apiClientMock) ListCodespaces(ctx context.Context, opts api.ListCodespacesOptions) ([]*api.Codespace, error) {
|
||||
func (mock *apiClientMock) ListCodespaces(ctx context.Context, opts codespacesAPI.ListCodespacesOptions) ([]*codespacesAPI.Codespace, error) {
|
||||
if mock.ListCodespacesFunc == nil {
|
||||
panic("apiClientMock.ListCodespacesFunc: method is nil but apiClient.ListCodespaces was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Ctx context.Context
|
||||
Opts api.ListCodespacesOptions
|
||||
Opts codespacesAPI.ListCodespacesOptions
|
||||
}{
|
||||
Ctx: ctx,
|
||||
Opts: opts,
|
||||
|
|
@ -726,11 +763,11 @@ func (mock *apiClientMock) ListCodespaces(ctx context.Context, opts api.ListCode
|
|||
// len(mockedapiClient.ListCodespacesCalls())
|
||||
func (mock *apiClientMock) ListCodespacesCalls() []struct {
|
||||
Ctx context.Context
|
||||
Opts api.ListCodespacesOptions
|
||||
Opts codespacesAPI.ListCodespacesOptions
|
||||
} {
|
||||
var calls []struct {
|
||||
Ctx context.Context
|
||||
Opts api.ListCodespacesOptions
|
||||
Opts codespacesAPI.ListCodespacesOptions
|
||||
}
|
||||
mock.lockListCodespaces.RLock()
|
||||
calls = mock.calls.ListCodespaces
|
||||
|
|
@ -739,7 +776,7 @@ func (mock *apiClientMock) ListCodespacesCalls() []struct {
|
|||
}
|
||||
|
||||
// ListDevContainers calls ListDevContainersFunc.
|
||||
func (mock *apiClientMock) ListDevContainers(ctx context.Context, repoID int, branch string, limit int) ([]api.DevContainerEntry, error) {
|
||||
func (mock *apiClientMock) ListDevContainers(ctx context.Context, repoID int, branch string, limit int) ([]codespacesAPI.DevContainerEntry, error) {
|
||||
if mock.ListDevContainersFunc == nil {
|
||||
panic("apiClientMock.ListDevContainersFunc: method is nil but apiClient.ListDevContainers was just called")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,28 @@
|
|||
package codespace
|
||||
|
||||
import (
|
||||
codespacesAPI "github.com/cli/cli/v2/internal/codespaces/api"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewRootCmd(app *App) *cobra.Command {
|
||||
func NewCmdCodespace(f *cmdutil.Factory) *cobra.Command {
|
||||
root := &cobra.Command{
|
||||
Use: "codespace",
|
||||
Short: "Connect to and manage codespaces",
|
||||
Use: "codespace",
|
||||
Short: "Connect to and manage codespaces",
|
||||
Aliases: []string{"cs"},
|
||||
GroupID: "core",
|
||||
}
|
||||
|
||||
app := NewApp(
|
||||
f.IOStreams,
|
||||
f,
|
||||
codespacesAPI.New(f),
|
||||
f.Browser,
|
||||
f.Remotes,
|
||||
)
|
||||
|
||||
root.AddCommand(newCodeCmd(app))
|
||||
root.AddCommand(newCreateCmd(app))
|
||||
root.AddCommand(newEditCmd(app))
|
||||
|
|
|
|||
51
pkg/cmd/config/clear-cache/clear_cache.go
Normal file
51
pkg/cmd/config/clear-cache/clear_cache.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package clearcache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/go-gh/v2/pkg/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type ClearCacheOptions struct {
|
||||
IO *iostreams.IOStreams
|
||||
CacheDir string
|
||||
}
|
||||
|
||||
func NewCmdConfigClearCache(f *cmdutil.Factory, runF func(*ClearCacheOptions) error) *cobra.Command {
|
||||
opts := &ClearCacheOptions{
|
||||
IO: f.IOStreams,
|
||||
CacheDir: config.CacheDir(),
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "clear-cache",
|
||||
Short: "Clear the cli cache",
|
||||
Example: heredoc.Doc(`
|
||||
# Clear the cli cache
|
||||
$ gh config clear-cache
|
||||
`),
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
return clearCacheRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func clearCacheRun(opts *ClearCacheOptions) error {
|
||||
if err := os.RemoveAll(opts.CacheDir); err != nil {
|
||||
return err
|
||||
}
|
||||
cs := opts.IO.ColorScheme()
|
||||
fmt.Fprintf(opts.IO.Out, "%s Cleared the cache\n", cs.SuccessIcon())
|
||||
return nil
|
||||
}
|
||||
32
pkg/cmd/config/clear-cache/clear_cache_test.go
Normal file
32
pkg/cmd/config/clear-cache/clear_cache_test.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package clearcache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestClearCacheRun(t *testing.T) {
|
||||
cacheDir := filepath.Join(t.TempDir(), "gh-cli-cache")
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
opts := &ClearCacheOptions{
|
||||
IO: ios,
|
||||
CacheDir: cacheDir,
|
||||
}
|
||||
|
||||
if err := os.Mkdir(opts.CacheDir, 0600); err != nil {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
if err := clearCacheRun(opts); err != nil {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
assert.NoDirExistsf(t, opts.CacheDir, fmt.Sprintf("Cache dir: %s still exists", opts.CacheDir))
|
||||
assert.Equal(t, "✓ Cleared the cache\n", stdout.String())
|
||||
assert.Equal(t, "", stderr.String())
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
cmdClearCache "github.com/cli/cli/v2/pkg/cmd/config/clear-cache"
|
||||
cmdGet "github.com/cli/cli/v2/pkg/cmd/config/get"
|
||||
cmdList "github.com/cli/cli/v2/pkg/cmd/config/list"
|
||||
cmdSet "github.com/cli/cli/v2/pkg/cmd/config/set"
|
||||
|
|
@ -35,6 +36,7 @@ func NewCmdConfig(f *cmdutil.Factory) *cobra.Command {
|
|||
cmd.AddCommand(cmdGet.NewCmdConfigGet(f, nil))
|
||||
cmd.AddCommand(cmdSet.NewCmdConfigSet(f, nil))
|
||||
cmd.AddCommand(cmdList.NewCmdConfigList(f, nil))
|
||||
cmd.AddCommand(cmdClearCache.NewCmdConfigClearCache(f, nil))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ var ssoURLRE = regexp.MustCompile(`\burl=([^;]+)`)
|
|||
|
||||
func New(appVersion string) *cmdutil.Factory {
|
||||
f := &cmdutil.Factory{
|
||||
AppVersion: appVersion,
|
||||
Config: configFunc(), // No factory dependencies
|
||||
ExecutableName: "gh",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
Repo: baseRepo,
|
||||
State: &tb,
|
||||
}
|
||||
err = prShared.MetadataSurvey(opts.IO, baseRepo, fetcher, &tb)
|
||||
err = prShared.MetadataSurvey(opts.Prompter, opts.IO, baseRepo, fetcher, &tb)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,16 +3,13 @@ package delete
|
|||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/issue/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/pkg/prompt"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
|
@ -22,16 +19,22 @@ type DeleteOptions struct {
|
|||
Config func() (config.Config, error)
|
||||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Prompter iprompter
|
||||
|
||||
SelectorArg string
|
||||
Confirmed bool
|
||||
}
|
||||
|
||||
type iprompter interface {
|
||||
ConfirmDeletion(string) error
|
||||
}
|
||||
|
||||
func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Command {
|
||||
opts := &DeleteOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
Config: f.Config,
|
||||
Prompter: f.Prompter,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
|
@ -79,22 +82,12 @@ func deleteRun(opts *DeleteOptions) error {
|
|||
// When executed in an interactive shell, require confirmation, unless
|
||||
// already provided. Otherwise skip confirmation.
|
||||
if opts.IO.CanPrompt() && !opts.Confirmed {
|
||||
answer := ""
|
||||
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
|
||||
err = prompt.SurveyAskOne(
|
||||
&survey.Input{
|
||||
Message: fmt.Sprintf("You're going to delete issue #%d. This action cannot be reversed. To confirm, type the issue number:", issue.Number),
|
||||
},
|
||||
&answer,
|
||||
)
|
||||
cs := opts.IO.ColorScheme()
|
||||
fmt.Printf("%s Deleted issues cannot be recovered.\n", cs.WarningIcon())
|
||||
err := opts.Prompter.ConfirmDeletion(fmt.Sprintf("%d", issue.Number))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
answerInt, err := strconv.Atoi(answer)
|
||||
if err != nil || answerInt != issue.Number {
|
||||
fmt.Fprintf(opts.IO.Out, "Issue #%d was not deleted.\n", issue.Number)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := apiDelete(httpClient, baseRepo, issue.ID); err != nil {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package delete
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
|
@ -9,16 +10,16 @@ import (
|
|||
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/pkg/prompt"
|
||||
"github.com/cli/cli/v2/test"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, error) {
|
||||
func runCommand(rt http.RoundTripper, pm *prompter.MockPrompter, isTTY bool, cli string) (*test.CmdOut, error) {
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
ios.SetStdoutTTY(isTTY)
|
||||
ios.SetStdinTTY(isTTY)
|
||||
|
|
@ -26,6 +27,7 @@ func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, err
|
|||
|
||||
factory := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
Prompter: pm,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: rt}, nil
|
||||
},
|
||||
|
|
@ -76,11 +78,10 @@ func TestIssueDelete(t *testing.T) {
|
|||
}),
|
||||
)
|
||||
|
||||
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
|
||||
as := prompt.NewAskStubber(t)
|
||||
as.StubPrompt("You're going to delete issue #13. This action cannot be reversed. To confirm, type the issue number:").AnswerWith("13")
|
||||
pm := prompter.NewMockPrompter(t)
|
||||
pm.RegisterConfirmDeletion("13", func(_ string) error { return nil })
|
||||
|
||||
output, err := runCommand(httpRegistry, true, "13")
|
||||
output, err := runCommand(httpRegistry, pm, true, "13")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `issue delete`: %v", err)
|
||||
}
|
||||
|
|
@ -112,7 +113,7 @@ func TestIssueDelete_confirm(t *testing.T) {
|
|||
}),
|
||||
)
|
||||
|
||||
output, err := runCommand(httpRegistry, true, "13 --confirm")
|
||||
output, err := runCommand(httpRegistry, nil, true, "13 --confirm")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `issue delete`: %v", err)
|
||||
}
|
||||
|
|
@ -137,19 +138,17 @@ func TestIssueDelete_cancel(t *testing.T) {
|
|||
} } }`),
|
||||
)
|
||||
|
||||
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
|
||||
as := prompt.NewAskStubber(t)
|
||||
as.StubPrompt("You're going to delete issue #13. This action cannot be reversed. To confirm, type the issue number:").AnswerWith("14")
|
||||
pm := prompter.NewMockPrompter(t)
|
||||
pm.RegisterConfirmDeletion("13", func(_ string) error {
|
||||
return errors.New("You entered 14")
|
||||
})
|
||||
|
||||
output, err := runCommand(httpRegistry, true, "13")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `issue delete`: %v", err)
|
||||
_, err := runCommand(httpRegistry, pm, true, "13")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
|
||||
r := regexp.MustCompile(`Issue #13 was not deleted`)
|
||||
|
||||
if !r.MatchString(output.String()) {
|
||||
t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.String())
|
||||
if err.Error() != "You entered 14" {
|
||||
t.Fatalf("got unexpected error '%s'", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,7 +165,7 @@ func TestIssueDelete_doesNotExist(t *testing.T) {
|
|||
`),
|
||||
)
|
||||
|
||||
_, err := runCommand(httpRegistry, true, "13")
|
||||
_, err := runCommand(httpRegistry, nil, true, "13")
|
||||
if err == nil || err.Error() != "GraphQL: Could not resolve to an Issue with the number of 13." {
|
||||
t.Errorf("error running command `issue delete`: %v", err)
|
||||
}
|
||||
|
|
@ -199,7 +198,7 @@ func TestIssueDelete_issuesDisabled(t *testing.T) {
|
|||
}`),
|
||||
)
|
||||
|
||||
_, err := runCommand(httpRegistry, true, "13")
|
||||
_, err := runCommand(httpRegistry, nil, true, "13")
|
||||
if err == nil || err.Error() != "the 'OWNER/REPO' repository has disabled issues" {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import (
|
|||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/context"
|
||||
"github.com/cli/cli/v2/git"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/tableprinter"
|
||||
"github.com/cli/cli/v2/pkg/cmd/issue/shared"
|
||||
|
|
@ -21,17 +20,16 @@ import (
|
|||
type DevelopOptions struct {
|
||||
HttpClient func() (*http.Client, error)
|
||||
GitClient *git.Client
|
||||
Config func() (config.Config, error)
|
||||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Remotes func() (context.Remotes, error)
|
||||
|
||||
IssueRepoSelector string
|
||||
IssueSelector string
|
||||
Name string
|
||||
BaseBranch string
|
||||
Checkout bool
|
||||
List bool
|
||||
IssueSelector string
|
||||
Name string
|
||||
BranchRepo string
|
||||
BaseBranch string
|
||||
Checkout bool
|
||||
List bool
|
||||
}
|
||||
|
||||
func NewCmdDevelop(f *cmdutil.Factory, runF func(*DevelopOptions) error) *cobra.Command {
|
||||
|
|
@ -39,200 +37,159 @@ func NewCmdDevelop(f *cmdutil.Factory, runF func(*DevelopOptions) error) *cobra.
|
|||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
GitClient: f.GitClient,
|
||||
Config: f.Config,
|
||||
BaseRepo: f.BaseRepo,
|
||||
Remotes: f.Remotes,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "develop [flags] {<number> | <url>}",
|
||||
Use: "develop {<number> | <url>}",
|
||||
Short: "Manage linked branches for an issue",
|
||||
Example: heredoc.Doc(`
|
||||
$ gh issue develop --list 123 # list branches for issue 123
|
||||
$ gh issue develop --list --issue-repo "github/cli" 123 # list branches for issue 123 in repo "github/cli"
|
||||
$ gh issue develop --list https://github.com/github/cli/issues/123 # list branches for issue 123 in repo "github/cli"
|
||||
$ gh issue develop 123 --name "my-branch" --base my-feature # create a branch for issue 123 based on the my-feature branch
|
||||
$ gh issue develop 123 --checkout # fetch and checkout the branch for issue 123 after creating it
|
||||
`),
|
||||
# List branches for issue 123
|
||||
$ gh issue develop --list 123
|
||||
|
||||
# List branches for issue 123 in repo cli/cli
|
||||
$ gh issue develop --list --repo cli/cli 123
|
||||
|
||||
# Create a branch for issue 123 based on the my-feature branch
|
||||
$ gh issue develop 123 --base my-feature
|
||||
|
||||
# Create a branch for issue 123 and checkout it out
|
||||
$ gh issue develop 123 --checkout
|
||||
|
||||
# Create a branch in repo monalisa/cli for issue 123 in repo cli/cli
|
||||
$ gh issue develop 123 --repo cli/cli --branch-repo monalisa/cli
|
||||
`),
|
||||
Args: cmdutil.ExactArgs(1, "issue number or url is required"),
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// This is all a hack to not break the deprecated issue-repo flag.
|
||||
// It will be removed in the near future and this hack can be removed at the same time.
|
||||
flags := cmd.Flags()
|
||||
if flags.Changed("issue-repo") {
|
||||
if flags.Changed("repo") {
|
||||
if flags.Changed("branch-repo") {
|
||||
return cmdutil.FlagErrorf("specify only `--repo` and `--branch-repo`")
|
||||
}
|
||||
branchRepo, _ := flags.GetString("repo")
|
||||
_ = flags.Set("branch-repo", branchRepo)
|
||||
}
|
||||
repo, _ := flags.GetString("issue-repo")
|
||||
_ = flags.Set("repo", repo)
|
||||
}
|
||||
if cmd.Parent() != nil {
|
||||
return cmd.Parent().PersistentPreRunE(cmd, args)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
opts.IssueSelector = args[0]
|
||||
if err := cmdutil.MutuallyExclusive("specify only one of `--list` or `--branch-repo`", opts.List, opts.BranchRepo != ""); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmdutil.MutuallyExclusive("specify only one of `--list` or `--base`", opts.List, opts.BaseBranch != ""); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmdutil.MutuallyExclusive("specify only one of `--list` or `--checkout`", opts.List, opts.Checkout); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmdutil.MutuallyExclusive("specify only one of `--list` or `--name`", opts.List, opts.Name != ""); err != nil {
|
||||
return err
|
||||
}
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
opts.IssueSelector = args[0]
|
||||
if opts.List {
|
||||
return developRunList(opts)
|
||||
}
|
||||
return developRunCreate(opts)
|
||||
return developRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
fl := cmd.Flags()
|
||||
fl.StringVar(&opts.BranchRepo, "branch-repo", "", "Name or URL of the repository where you want to create your new branch")
|
||||
fl.StringVarP(&opts.BaseBranch, "base", "b", "", "Name of the base branch you want to make your new branch from")
|
||||
fl.BoolVarP(&opts.Checkout, "checkout", "c", false, "Checkout the branch after creating it")
|
||||
fl.StringVarP(&opts.IssueRepoSelector, "issue-repo", "i", "", "Name or URL of the issue's repository")
|
||||
fl.BoolVarP(&opts.List, "list", "l", false, "List linked branches for the issue")
|
||||
fl.StringVarP(&opts.Name, "name", "n", "", "Name of the branch to create")
|
||||
|
||||
var issueRepoSelector string
|
||||
fl.StringVarP(&issueRepoSelector, "issue-repo", "i", "", "Name or URL of the issue's repository")
|
||||
_ = cmd.Flags().MarkDeprecated("issue-repo", "use `--repo` instead")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func developRunCreate(opts *DevelopOptions) (err error) {
|
||||
func developRun(opts *DevelopOptions) error {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
baseRepo, err := opts.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts.IO.StartProgressIndicator()
|
||||
err = api.CheckLinkedBranchFeature(apiClient, baseRepo.RepoHost())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repo, err := api.GitHubRepo(apiClient, baseRepo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
issueNumber, issueRepo, err := issueMetadata(opts.IssueSelector, opts.IssueRepoSelector, baseRepo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The mutation requires the issue id, not just its number
|
||||
issue, _, err := shared.IssueFromArgWithFields(httpClient, func() (ghrepo.Interface, error) { return issueRepo, nil }, fmt.Sprint(issueNumber), []string{"id"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The mutation takes an oid instead of a branch name as it's a more stable reference
|
||||
oid, default_branch_oid, err := api.FindBaseOid(apiClient, repo, opts.BaseBranch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if oid == "" {
|
||||
oid = default_branch_oid
|
||||
}
|
||||
|
||||
// get the oid of the branch from the base repo
|
||||
params := map[string]interface{}{
|
||||
"issueId": issue.ID,
|
||||
"name": opts.Name,
|
||||
"oid": oid,
|
||||
"repositoryId": repo.ID,
|
||||
}
|
||||
|
||||
ref, err := api.CreateBranchIssueReference(apiClient, repo, params)
|
||||
issue, issueRepo, err := shared.IssueFromArgWithFields(httpClient, opts.BaseRepo, opts.IssueSelector, []string{"id", "number"})
|
||||
opts.IO.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
baseRepo.RepoHost()
|
||||
fmt.Fprintf(opts.IO.Out, "%s/%s/%s/tree/%s\n", baseRepo.RepoHost(), baseRepo.RepoOwner(), baseRepo.RepoName(), ref.BranchName)
|
||||
return checkoutBranch(opts, baseRepo, ref.BranchName)
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
opts.IO.StartProgressIndicator()
|
||||
err = api.CheckLinkedBranchFeature(apiClient, issueRepo.RepoHost())
|
||||
opts.IO.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.List {
|
||||
return developRunList(opts, apiClient, issueRepo, issue)
|
||||
}
|
||||
return developRunCreate(opts, apiClient, issueRepo, issue)
|
||||
}
|
||||
|
||||
// If the issue is in the base repo, we can use the issue number directly. Otherwise, we need to use the issue's url or the IssueRepoSelector argument.
|
||||
// If the repo from the URL doesn't match the IssueRepoSelector argument, we error.
|
||||
func issueMetadata(issueSelector string, issueRepoSelector string, baseRepo ghrepo.Interface) (issueNumber int, issueFlagRepo ghrepo.Interface, err error) {
|
||||
var targetRepo ghrepo.Interface
|
||||
if issueRepoSelector != "" {
|
||||
issueFlagRepo, err = ghrepo.FromFullNameWithHost(issueRepoSelector, baseRepo.RepoHost())
|
||||
func developRunCreate(opts *DevelopOptions, apiClient *api.Client, issueRepo ghrepo.Interface, issue *api.Issue) error {
|
||||
branchRepo := issueRepo
|
||||
var repoID string
|
||||
if opts.BranchRepo != "" {
|
||||
var err error
|
||||
branchRepo, err = ghrepo.FromFullName(opts.BranchRepo)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if issueFlagRepo != nil {
|
||||
targetRepo = issueFlagRepo
|
||||
}
|
||||
|
||||
issueNumber, issueArgRepo, err := shared.IssueNumberAndRepoFromArg(issueSelector)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
if issueArgRepo != nil {
|
||||
targetRepo = issueArgRepo
|
||||
|
||||
if issueFlagRepo != nil {
|
||||
differentOwner := (issueFlagRepo.RepoOwner() != issueArgRepo.RepoOwner())
|
||||
differentName := (issueFlagRepo.RepoName() != issueArgRepo.RepoName())
|
||||
if differentOwner || differentName {
|
||||
return 0, nil, fmt.Errorf("issue repo in url %s/%s does not match the repo from --issue-repo %s/%s", issueArgRepo.RepoOwner(), issueArgRepo.RepoName(), issueFlagRepo.RepoOwner(), issueFlagRepo.RepoName())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if issueFlagRepo == nil && issueArgRepo == nil {
|
||||
targetRepo = baseRepo
|
||||
}
|
||||
|
||||
if targetRepo == nil {
|
||||
return 0, nil, fmt.Errorf("could not determine issue repo")
|
||||
}
|
||||
|
||||
return issueNumber, targetRepo, nil
|
||||
}
|
||||
|
||||
func printLinkedBranches(io *iostreams.IOStreams, branches []api.LinkedBranch) {
|
||||
cs := io.ColorScheme()
|
||||
table := tableprinter.New(io)
|
||||
|
||||
for _, branch := range branches {
|
||||
table.AddField(branch.BranchName, tableprinter.WithColor(cs.ColorFromString("cyan")))
|
||||
if io.CanPrompt() {
|
||||
table.AddField(branch.Url())
|
||||
}
|
||||
table.EndRow()
|
||||
}
|
||||
|
||||
_ = table.Render()
|
||||
}
|
||||
|
||||
func developRunList(opts *DevelopOptions) (err error) {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
baseRepo, err := opts.BaseRepo()
|
||||
opts.IO.StartProgressIndicator()
|
||||
repoID, branchID, err := api.FindRepoBranchID(apiClient, branchRepo, opts.BaseBranch)
|
||||
opts.IO.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts.IO.StartProgressIndicator()
|
||||
|
||||
err = api.CheckLinkedBranchFeature(apiClient, baseRepo.RepoHost())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
issueNumber, issueRepo, err := issueMetadata(opts.IssueSelector, opts.IssueRepoSelector, baseRepo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
branches, err := api.ListLinkedBranches(apiClient, issueRepo, issueNumber)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
branchName, err := api.CreateLinkedBranch(apiClient, branchRepo.RepoHost(), repoID, issue.ID, branchID, opts.Name)
|
||||
opts.IO.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(opts.IO.Out, "%s/%s/%s/tree/%s\n", branchRepo.RepoHost(), branchRepo.RepoOwner(), branchRepo.RepoName(), branchName)
|
||||
|
||||
return checkoutBranch(opts, branchRepo, branchName)
|
||||
}
|
||||
|
||||
func developRunList(opts *DevelopOptions, apiClient *api.Client, issueRepo ghrepo.Interface, issue *api.Issue) error {
|
||||
opts.IO.StartProgressIndicator()
|
||||
branches, err := api.ListLinkedBranches(apiClient, issueRepo, issue.Number)
|
||||
opts.IO.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(branches) == 0 {
|
||||
return cmdutil.NewNoResultsError(fmt.Sprintf("no linked branches found for %s/%s#%d", issueRepo.RepoOwner(), issueRepo.RepoName(), issueNumber))
|
||||
return cmdutil.NewNoResultsError(fmt.Sprintf("no linked branches found for %s/%s#%d", issueRepo.RepoOwner(), issueRepo.RepoName(), issue.Number))
|
||||
}
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
fmt.Fprintf(opts.IO.Out, "\nShowing linked branches for %s/%s#%d\n\n", issueRepo.RepoOwner(), issueRepo.RepoName(), issueNumber)
|
||||
fmt.Fprintf(opts.IO.Out, "\nShowing linked branches for %s/%s#%d\n\n", issueRepo.RepoOwner(), issueRepo.RepoName(), issue.Number)
|
||||
}
|
||||
|
||||
printLinkedBranches(opts.IO, branches)
|
||||
|
|
@ -240,7 +197,18 @@ func developRunList(opts *DevelopOptions) (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
func checkoutBranch(opts *DevelopOptions, baseRepo ghrepo.Interface, checkoutBranch string) (err error) {
|
||||
func printLinkedBranches(io *iostreams.IOStreams, branches []api.LinkedBranch) {
|
||||
cs := io.ColorScheme()
|
||||
table := tableprinter.New(io)
|
||||
for _, branch := range branches {
|
||||
table.AddField(branch.BranchName, tableprinter.WithColor(cs.ColorFromString("cyan")))
|
||||
table.AddField(branch.URL)
|
||||
table.EndRow()
|
||||
}
|
||||
_ = table.Render()
|
||||
}
|
||||
|
||||
func checkoutBranch(opts *DevelopOptions, branchRepo ghrepo.Interface, checkoutBranch string) (err error) {
|
||||
remotes, err := opts.Remotes()
|
||||
if err != nil {
|
||||
// If the user specified the branch to be checked out and no remotes are found
|
||||
|
|
@ -253,7 +221,7 @@ func checkoutBranch(opts *DevelopOptions, baseRepo ghrepo.Interface, checkoutBra
|
|||
}
|
||||
}
|
||||
|
||||
baseRemote, err := remotes.FindByRepo(baseRepo.RepoOwner(), baseRepo.RepoName())
|
||||
baseRemote, err := remotes.FindByRepo(branchRepo.RepoOwner(), branchRepo.RepoName())
|
||||
if err != nil {
|
||||
// If the user specified the branch to be checked out and no remote matches the
|
||||
// base repo, then display an error. Otherwise bail out silently.
|
||||
|
|
|
|||
|
|
@ -1,418 +1,307 @@
|
|||
package develop
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/context"
|
||||
"github.com/cli/cli/v2/git"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/run"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/pkg/prompt"
|
||||
"github.com/cli/cli/v2/test"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_developRun(t *testing.T) {
|
||||
featureEnabledPayload := `{
|
||||
"data": {
|
||||
"LinkedBranch": {
|
||||
"fields": [
|
||||
{
|
||||
"name": "id"
|
||||
},
|
||||
{
|
||||
"name": "ref"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`
|
||||
func TestNewCmdDevelop(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
output DevelopOptions
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "no argument",
|
||||
input: "",
|
||||
output: DevelopOptions{},
|
||||
wantErr: true,
|
||||
errMsg: "issue number or url is required",
|
||||
},
|
||||
{
|
||||
name: "issue number",
|
||||
input: "1",
|
||||
output: DevelopOptions{
|
||||
IssueSelector: "1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "issue url",
|
||||
input: "https://github.com/cli/cli/issues/1",
|
||||
output: DevelopOptions{
|
||||
IssueSelector: "https://github.com/cli/cli/issues/1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "branch-repo flag",
|
||||
input: "1 --branch-repo owner/repo",
|
||||
output: DevelopOptions{
|
||||
IssueSelector: "1",
|
||||
BranchRepo: "owner/repo",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "base flag",
|
||||
input: "1 --base feature",
|
||||
output: DevelopOptions{
|
||||
IssueSelector: "1",
|
||||
BaseBranch: "feature",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "checkout flag",
|
||||
input: "1 --checkout",
|
||||
output: DevelopOptions{
|
||||
IssueSelector: "1",
|
||||
Checkout: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list flag",
|
||||
input: "1 --list",
|
||||
output: DevelopOptions{
|
||||
IssueSelector: "1",
|
||||
List: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "name flag",
|
||||
input: "1 --name feature",
|
||||
output: DevelopOptions{
|
||||
IssueSelector: "1",
|
||||
Name: "feature",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "issue-repo flag",
|
||||
input: "1 --issue-repo cli/cli",
|
||||
output: DevelopOptions{
|
||||
IssueSelector: "1",
|
||||
},
|
||||
wantStdout: "Flag --issue-repo has been deprecated, use `--repo` instead\n",
|
||||
},
|
||||
{
|
||||
name: "list and branch repo flags",
|
||||
input: "1 --list --branch-repo owner/repo",
|
||||
wantErr: true,
|
||||
errMsg: "specify only one of `--list` or `--branch-repo`",
|
||||
},
|
||||
{
|
||||
name: "list and base flags",
|
||||
input: "1 --list --base feature",
|
||||
wantErr: true,
|
||||
errMsg: "specify only one of `--list` or `--base`",
|
||||
},
|
||||
{
|
||||
name: "list and checkout flags",
|
||||
input: "1 --list --checkout",
|
||||
wantErr: true,
|
||||
errMsg: "specify only one of `--list` or `--checkout`",
|
||||
},
|
||||
{
|
||||
name: "list and name flags",
|
||||
input: "1 --list --name my-branch",
|
||||
wantErr: true,
|
||||
errMsg: "specify only one of `--list` or `--name`",
|
||||
},
|
||||
}
|
||||
|
||||
featureDisabledPayload := `{ "data": { "LinkedBranch": null } }`
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, stdOut, stdErr := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
}
|
||||
argv, err := shlex.Split(tt.input)
|
||||
assert.NoError(t, err)
|
||||
var gotOpts *DevelopOptions
|
||||
cmd := NewCmdDevelop(f, func(opts *DevelopOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
cmd.SetArgs(argv)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(stdOut)
|
||||
cmd.SetErr(stdErr)
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantErr {
|
||||
assert.EqualError(t, err, tt.errMsg)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.output.IssueSelector, gotOpts.IssueSelector)
|
||||
assert.Equal(t, tt.output.Name, gotOpts.Name)
|
||||
assert.Equal(t, tt.output.BaseBranch, gotOpts.BaseBranch)
|
||||
assert.Equal(t, tt.output.Checkout, gotOpts.Checkout)
|
||||
assert.Equal(t, tt.output.List, gotOpts.List)
|
||||
assert.Equal(t, tt.wantStdout, stdOut.String())
|
||||
assert.Equal(t, tt.wantStderr, stdErr.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDevelopRun(t *testing.T) {
|
||||
featureEnabledPayload := `{"data":{"LinkedBranch":{"fields":[{"name":"id"},{"name":"ref"}]}}}`
|
||||
featureDisabledPayload := `{"data":{"LinkedBranch":null}}`
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(*DevelopOptions, *testing.T) func()
|
||||
opts *DevelopOptions
|
||||
cmdStubs func(*run.CommandStubber)
|
||||
runStubs func(*run.CommandStubber)
|
||||
remotes map[string]string
|
||||
askStubs func(*prompt.AskStubber) // TODO eventually migrate to PrompterMock
|
||||
httpStubs func(*httpmock.Registry, *testing.T)
|
||||
expectedOut string
|
||||
expectedErrOut string
|
||||
expectedBrowse string
|
||||
wantErr string
|
||||
tty bool
|
||||
}{
|
||||
{
|
||||
name: "list branches for an issue",
|
||||
setup: func(opts *DevelopOptions, t *testing.T) func() {
|
||||
opts.IssueSelector = "42"
|
||||
opts.List = true
|
||||
return func() {}
|
||||
name: "returns an error when the feature is not supported by the API",
|
||||
opts: &DevelopOptions{
|
||||
IssueSelector: "42",
|
||||
List: true,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query LinkedBranch_fields\b`),
|
||||
httpmock.StringResponse(featureEnabledPayload),
|
||||
httpmock.GraphQL(`query IssueByNumber\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id":"SOMEID","number":42}}}}`),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query BranchIssueReferenceListLinkedBranches\b`),
|
||||
httpmock.GraphQLQuery(`{
|
||||
"data": {
|
||||
"repository": {
|
||||
"issue": {
|
||||
"linkedBranches": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"ref": {
|
||||
"name": "foo"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"ref": {
|
||||
"name": "bar"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`, func(query string, inputs map[string]interface{}) {
|
||||
assert.Equal(t, float64(42), inputs["issueNumber"])
|
||||
assert.Equal(t, "OWNER", inputs["repositoryOwner"])
|
||||
assert.Equal(t, "REPO", inputs["repositoryName"])
|
||||
}))
|
||||
},
|
||||
expectedOut: "foo\nbar\n",
|
||||
},
|
||||
{
|
||||
name: "list branches for an issue in tty",
|
||||
setup: func(opts *DevelopOptions, t *testing.T) func() {
|
||||
opts.IssueSelector = "42"
|
||||
opts.List = true
|
||||
return func() {}
|
||||
},
|
||||
tty: true,
|
||||
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query LinkedBranch_fields\b`),
|
||||
httpmock.StringResponse(featureEnabledPayload),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query BranchIssueReferenceListLinkedBranches\b`),
|
||||
httpmock.GraphQLQuery(`{
|
||||
"data": {
|
||||
"repository": {
|
||||
"issue": {
|
||||
"linkedBranches": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"ref": {
|
||||
"name": "foo",
|
||||
"repository": {
|
||||
"url": "http://github.localhost/OWNER/REPO"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"ref": {
|
||||
"name": "bar",
|
||||
"repository": {
|
||||
"url": "http://github.localhost/OWNER/OTHER-REPO"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`, func(query string, inputs map[string]interface{}) {
|
||||
assert.Equal(t, float64(42), inputs["issueNumber"])
|
||||
assert.Equal(t, "OWNER", inputs["repositoryOwner"])
|
||||
assert.Equal(t, "REPO", inputs["repositoryName"])
|
||||
}))
|
||||
},
|
||||
expectedOut: "\nShowing linked branches for OWNER/REPO#42\n\nfoo http://github.localhost/OWNER/REPO/tree/foo\nbar http://github.localhost/OWNER/OTHER-REPO/tree/bar\n",
|
||||
},
|
||||
{
|
||||
name: "list branches for an issue providing an issue url",
|
||||
setup: func(opts *DevelopOptions, t *testing.T) func() {
|
||||
opts.IssueSelector = "https://github.com/cli/test-repo/issues/42"
|
||||
opts.List = true
|
||||
return func() {}
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query LinkedBranch_fields\b`),
|
||||
httpmock.StringResponse(featureEnabledPayload),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query BranchIssueReferenceListLinkedBranches\b`),
|
||||
httpmock.GraphQLQuery(`{
|
||||
"data": {
|
||||
"repository": {
|
||||
"issue": {
|
||||
"linkedBranches": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"ref": {
|
||||
"name": "foo"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"ref": {
|
||||
"name": "bar"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`, func(query string, inputs map[string]interface{}) {
|
||||
assert.Equal(t, float64(42), inputs["issueNumber"])
|
||||
assert.Equal(t, "cli", inputs["repositoryOwner"])
|
||||
assert.Equal(t, "test-repo", inputs["repositoryName"])
|
||||
}))
|
||||
},
|
||||
expectedOut: "foo\nbar\n",
|
||||
},
|
||||
{
|
||||
name: "list branches for an issue providing an issue repo",
|
||||
setup: func(opts *DevelopOptions, t *testing.T) func() {
|
||||
opts.IssueSelector = "42"
|
||||
opts.IssueRepoSelector = "cli/test-repo"
|
||||
opts.List = true
|
||||
return func() {}
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query LinkedBranch_fields\b`),
|
||||
httpmock.StringResponse(featureEnabledPayload),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query BranchIssueReferenceListLinkedBranches\b`),
|
||||
httpmock.GraphQLQuery(`{
|
||||
"data": {
|
||||
"repository": {
|
||||
"issue": {
|
||||
"linkedBranches": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"ref": {
|
||||
"name": "foo"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"ref": {
|
||||
"name": "bar"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`, func(query string, inputs map[string]interface{}) {
|
||||
assert.Equal(t, float64(42), inputs["issueNumber"])
|
||||
assert.Equal(t, "cli", inputs["repositoryOwner"])
|
||||
assert.Equal(t, "test-repo", inputs["repositoryName"])
|
||||
}))
|
||||
},
|
||||
expectedOut: "foo\nbar\n",
|
||||
},
|
||||
{
|
||||
name: "list branches for an issue providing an issue url and specifying the same repo works",
|
||||
setup: func(opts *DevelopOptions, t *testing.T) func() {
|
||||
opts.IssueSelector = "https://github.com/cli/test-repo/issues/42"
|
||||
opts.IssueRepoSelector = "cli/test-repo"
|
||||
opts.List = true
|
||||
return func() {}
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query LinkedBranch_fields\b`),
|
||||
httpmock.StringResponse(featureEnabledPayload),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query BranchIssueReferenceListLinkedBranches\b`),
|
||||
httpmock.GraphQLQuery(`{
|
||||
"data": {
|
||||
"repository": {
|
||||
"issue": {
|
||||
"linkedBranches": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"ref": {
|
||||
"name": "foo"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"ref": {
|
||||
"name": "bar"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`, func(query string, inputs map[string]interface{}) {
|
||||
assert.Equal(t, float64(42), inputs["issueNumber"])
|
||||
assert.Equal(t, "cli", inputs["repositoryOwner"])
|
||||
assert.Equal(t, "test-repo", inputs["repositoryName"])
|
||||
}))
|
||||
},
|
||||
expectedOut: "foo\nbar\n",
|
||||
},
|
||||
{
|
||||
name: "list branches for an issue providing an issue url and specifying a different repo returns an error",
|
||||
setup: func(opts *DevelopOptions, t *testing.T) func() {
|
||||
opts.IssueSelector = "https://github.com/cli/test-repo/issues/42"
|
||||
opts.IssueRepoSelector = "cli/other"
|
||||
opts.List = true
|
||||
return func() {}
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query LinkedBranch_fields\b`),
|
||||
httpmock.StringResponse(featureEnabledPayload),
|
||||
)
|
||||
},
|
||||
wantErr: "issue repo in url cli/test-repo does not match the repo from --issue-repo cli/other",
|
||||
},
|
||||
{
|
||||
name: "returns an error when the feature isn't enabled in the GraphQL API",
|
||||
setup: func(opts *DevelopOptions, t *testing.T) func() {
|
||||
opts.IssueSelector = "https://github.com/cli/test-repo/issues/42"
|
||||
opts.List = true
|
||||
return func() {}
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query LinkedBranch_fields\b`),
|
||||
httpmock.GraphQL(`query LinkedBranchFeature\b`),
|
||||
httpmock.StringResponse(featureDisabledPayload),
|
||||
)
|
||||
},
|
||||
wantErr: "the `gh issue develop` command is not currently available",
|
||||
},
|
||||
{
|
||||
name: "develop new branch with name specified",
|
||||
setup: func(opts *DevelopOptions, t *testing.T) func() {
|
||||
opts.Name = "my-branch"
|
||||
opts.BaseBranch = "main"
|
||||
opts.IssueSelector = "123"
|
||||
return func() {}
|
||||
},
|
||||
remotes: map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
name: "list branches for an issue",
|
||||
opts: &DevelopOptions{
|
||||
IssueSelector: "42",
|
||||
List: true,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query LinkedBranch_fields\b`),
|
||||
httpmock.GraphQL(`query IssueByNumber\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id":"SOMEID","number":42}}}}`),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query LinkedBranchFeature\b`),
|
||||
httpmock.StringResponse(featureEnabledPayload),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query RepositoryInfo\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"id": "REPOID",
|
||||
"hasIssuesEnabled": true
|
||||
} } }`),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query IssueByNumber\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{ "hasIssuesEnabled": true, "issue":{"id": "yar", "number":123, "title":"my issue"} }}}`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query BranchIssueReferenceFindBaseOid\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{"ref":{"target":{"oid":"123"}}}}}`))
|
||||
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`(?s)mutation CreateLinkedBranch\b.*issueId: \$issueId,\s+name: \$name,\s+oid: \$oid,`),
|
||||
httpmock.GraphQLQuery(`{ "data": { "createLinkedBranch": { "linkedBranch": {"id": "2", "ref": {"name": "my-branch"} } } } }`,
|
||||
func(query string, inputs map[string]interface{}) {
|
||||
assert.Equal(t, "REPOID", inputs["repositoryId"])
|
||||
assert.Equal(t, "my-branch", inputs["name"])
|
||||
assert.Equal(t, "yar", inputs["issueId"])
|
||||
}),
|
||||
)
|
||||
httpmock.GraphQL(`query ListLinkedBranches\b`),
|
||||
httpmock.GraphQLQuery(`
|
||||
{"data":{"repository":{"issue":{"linkedBranches":{"nodes":[{"ref":{"name":"foo","repository":{"url":"https://github.com/OWNER/REPO"}}},{"ref":{"name":"bar","repository":{"url":"https://github.com/OWNER/REPO"}}}]}}}}}
|
||||
`, func(query string, inputs map[string]interface{}) {
|
||||
assert.Equal(t, float64(42), inputs["number"])
|
||||
assert.Equal(t, "OWNER", inputs["owner"])
|
||||
assert.Equal(t, "REPO", inputs["name"])
|
||||
}))
|
||||
},
|
||||
runStubs: func(cs *run.CommandStubber) {
|
||||
cs.Register(`git fetch origin \+refs/heads/my-branch:refs/remotes/origin/my-branch`, 0, "")
|
||||
},
|
||||
expectedOut: "github.com/OWNER/REPO/tree/my-branch\n",
|
||||
expectedOut: "foo\thttps://github.com/OWNER/REPO/tree/foo\nbar\thttps://github.com/OWNER/REPO/tree/bar\n",
|
||||
},
|
||||
{
|
||||
name: "develop new branch without a name provided omits the param from the mutation",
|
||||
setup: func(opts *DevelopOptions, t *testing.T) func() {
|
||||
opts.Name = ""
|
||||
opts.BaseBranch = "main"
|
||||
opts.IssueSelector = "123"
|
||||
return func() {}
|
||||
name: "list branches for an issue in tty",
|
||||
opts: &DevelopOptions{
|
||||
IssueSelector: "42",
|
||||
List: true,
|
||||
},
|
||||
tty: true,
|
||||
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query IssueByNumber\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id":"SOMEID","number":42}}}}`),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query LinkedBranchFeature\b`),
|
||||
httpmock.StringResponse(featureEnabledPayload),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query ListLinkedBranches\b`),
|
||||
httpmock.GraphQLQuery(`
|
||||
{"data":{"repository":{"issue":{"linkedBranches":{"nodes":[{"ref":{"name":"foo","repository":{"url":"https://github.com/OWNER/REPO"}}},{"ref":{"name":"bar","repository":{"url":"https://github.com/OWNER/OTHER-REPO"}}}]}}}}}
|
||||
`, func(query string, inputs map[string]interface{}) {
|
||||
assert.Equal(t, float64(42), inputs["number"])
|
||||
assert.Equal(t, "OWNER", inputs["owner"])
|
||||
assert.Equal(t, "REPO", inputs["name"])
|
||||
}))
|
||||
},
|
||||
expectedOut: "\nShowing linked branches for OWNER/REPO#42\n\nfoo https://github.com/OWNER/REPO/tree/foo\nbar https://github.com/OWNER/OTHER-REPO/tree/bar\n",
|
||||
},
|
||||
{
|
||||
name: "list branches for an issue providing an issue url",
|
||||
opts: &DevelopOptions{
|
||||
IssueSelector: "https://github.com/cli/cli/issues/42",
|
||||
List: true,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query IssueByNumber\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id":"SOMEID","number":42}}}}`),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query LinkedBranchFeature\b`),
|
||||
httpmock.StringResponse(featureEnabledPayload),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query ListLinkedBranches\b`),
|
||||
httpmock.GraphQLQuery(`
|
||||
{"data":{"repository":{"issue":{"linkedBranches":{"nodes":[{"ref":{"name":"foo","repository":{"url":"https://github.com/OWNER/REPO"}}},{"ref":{"name":"bar","repository":{"url":"https://github.com/OWNER/OTHER-REPO"}}}]}}}}}
|
||||
`, func(query string, inputs map[string]interface{}) {
|
||||
assert.Equal(t, float64(42), inputs["number"])
|
||||
assert.Equal(t, "cli", inputs["owner"])
|
||||
assert.Equal(t, "cli", inputs["name"])
|
||||
}))
|
||||
},
|
||||
expectedOut: "foo\thttps://github.com/OWNER/REPO/tree/foo\nbar\thttps://github.com/OWNER/OTHER-REPO/tree/bar\n",
|
||||
},
|
||||
{
|
||||
name: "develop new branch",
|
||||
opts: &DevelopOptions{
|
||||
IssueSelector: "123",
|
||||
},
|
||||
remotes: map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query LinkedBranch_fields\b`),
|
||||
httpmock.GraphQL(`query LinkedBranchFeature\b`),
|
||||
httpmock.StringResponse(featureEnabledPayload),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query RepositoryInfo\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"id": "REPOID",
|
||||
"hasIssuesEnabled": true
|
||||
} } }`),
|
||||
httpmock.GraphQL(`query IssueByNumber\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id": "SOMEID","number":123,"title":"my issue"}}}}`),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query IssueByNumber\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{ "hasIssuesEnabled": true, "issue":{"id": "yar", "number":123, "title":"my issue"} }}}`))
|
||||
httpmock.GraphQL(`query FindRepoBranchID\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{"id":"REPOID","defaultBranchRef":{"target":{"oid":"DEFAULTOID"}},"ref":{"target":{"oid":""}}}}}`),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query BranchIssueReferenceFindBaseOid\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{"ref":{"target":{"oid":"123"}}}}}`))
|
||||
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`(?s)mutation CreateLinkedBranch\b.*\$oid: GitObjectID!, \$repositoryId:.*issueId: \$issueId,\s+oid: \$oid,`),
|
||||
httpmock.GraphQLQuery(`{ "data": { "createLinkedBranch": { "linkedBranch": {"id": "2", "ref": {"name": "my-issue-1"} } } } }`,
|
||||
func(query string, inputs map[string]interface{}) {
|
||||
httpmock.GraphQL(`mutation CreateLinkedBranch\b`),
|
||||
httpmock.GraphQLMutation(`{"data":{"createLinkedBranch":{"linkedBranch":{"id":"2","ref":{"name":"my-issue-1"}}}}}`,
|
||||
func(inputs map[string]interface{}) {
|
||||
assert.Equal(t, "REPOID", inputs["repositoryId"])
|
||||
assert.Equal(t, "", inputs["name"])
|
||||
assert.Equal(t, "yar", inputs["issueId"])
|
||||
assert.Equal(t, "SOMEID", inputs["issueId"])
|
||||
assert.Equal(t, "DEFAULTOID", inputs["oid"])
|
||||
}),
|
||||
)
|
||||
},
|
||||
|
|
@ -422,123 +311,198 @@ func Test_developRun(t *testing.T) {
|
|||
expectedOut: "github.com/OWNER/REPO/tree/my-issue-1\n",
|
||||
},
|
||||
{
|
||||
name: "develop providing an issue url and specifying a different repo returns an error",
|
||||
setup: func(opts *DevelopOptions, t *testing.T) func() {
|
||||
opts.IssueSelector = "https://github.com/cli/test-repo/issues/42"
|
||||
opts.IssueRepoSelector = "cli/other"
|
||||
return func() {}
|
||||
name: "develop new branch in diffferent repo than issue",
|
||||
opts: &DevelopOptions{
|
||||
IssueSelector: "123",
|
||||
BranchRepo: "OWNER2/REPO",
|
||||
},
|
||||
remotes: map[string]string{
|
||||
"origin": "OWNER2/REPO",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query LinkedBranch_fields\b`),
|
||||
httpmock.GraphQL(`query LinkedBranchFeature\b`),
|
||||
httpmock.StringResponse(featureEnabledPayload),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query RepositoryInfo\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"id": "REPOID",
|
||||
"hasIssuesEnabled": true
|
||||
} } }`),
|
||||
httpmock.GraphQL(`query IssueByNumber\b`),
|
||||
httpmock.GraphQLQuery(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id": "SOMEID","number":123,"title":"my issue"}}}}`,
|
||||
func(_ string, inputs map[string]interface{}) {
|
||||
assert.Equal(t, "OWNER", inputs["owner"])
|
||||
assert.Equal(t, "REPO", inputs["repo"])
|
||||
assert.Equal(t, float64(123), inputs["number"])
|
||||
}),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query FindRepoBranchID\b`),
|
||||
httpmock.GraphQLQuery(`{"data":{"repository":{"id":"REPOID","defaultBranchRef":{"target":{"oid":"DEFAULTOID"}},"ref":{"target":{"oid":""}}}}}`,
|
||||
func(_ string, inputs map[string]interface{}) {
|
||||
assert.Equal(t, "OWNER2", inputs["owner"])
|
||||
assert.Equal(t, "REPO", inputs["name"])
|
||||
}),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`mutation CreateLinkedBranch\b`),
|
||||
httpmock.GraphQLMutation(`{"data":{"createLinkedBranch":{"linkedBranch":{"id":"2","ref":{"name":"my-issue-1"}}}}}`,
|
||||
func(inputs map[string]interface{}) {
|
||||
assert.Equal(t, "REPOID", inputs["repositoryId"])
|
||||
assert.Equal(t, "SOMEID", inputs["issueId"])
|
||||
assert.Equal(t, "DEFAULTOID", inputs["oid"])
|
||||
}),
|
||||
)
|
||||
},
|
||||
wantErr: "issue repo in url cli/test-repo does not match the repo from --issue-repo cli/other",
|
||||
runStubs: func(cs *run.CommandStubber) {
|
||||
cs.Register(`git fetch origin \+refs/heads/my-issue-1:refs/remotes/origin/my-issue-1`, 0, "")
|
||||
},
|
||||
expectedOut: "github.com/OWNER2/REPO/tree/my-issue-1\n",
|
||||
},
|
||||
{
|
||||
name: "develop new branch with checkout when the branch exists locally",
|
||||
setup: func(opts *DevelopOptions, t *testing.T) func() {
|
||||
opts.Name = "my-branch"
|
||||
opts.BaseBranch = "main"
|
||||
opts.IssueSelector = "123"
|
||||
opts.Checkout = true
|
||||
return func() {}
|
||||
name: "develop new branch with name and base specified",
|
||||
opts: &DevelopOptions{
|
||||
Name: "my-branch",
|
||||
BaseBranch: "main",
|
||||
IssueSelector: "123",
|
||||
},
|
||||
remotes: map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query LinkedBranch_fields\b`),
|
||||
httpmock.GraphQL(`query LinkedBranchFeature\b`),
|
||||
httpmock.StringResponse(featureEnabledPayload),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query RepositoryInfo\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"id": "REPOID",
|
||||
"hasIssuesEnabled": true
|
||||
} } }`),
|
||||
httpmock.GraphQL(`query IssueByNumber\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{ "hasIssuesEnabled":true,"issue":{"id":"SOMEID","number":123,"title":"my issue"}}}}`),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query IssueByNumber\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{ "hasIssuesEnabled": true, "issue":{"id": "yar", "number":123, "title":"my issue"} }}}`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query BranchIssueReferenceFindBaseOid\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{"ref":{"target":{"oid":"123"}}}}}`))
|
||||
|
||||
httpmock.GraphQL(`query FindRepoBranchID\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{"id":"REPOID","ref":{"target":{"oid":"OID"}}}}}`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`mutation CreateLinkedBranch\b`),
|
||||
httpmock.GraphQLQuery(`{ "data": { "createLinkedBranch": { "linkedBranch": {"id": "2", "ref": {"name": "my-branch"} } } } }`,
|
||||
func(query string, inputs map[string]interface{}) {
|
||||
httpmock.GraphQLMutation(`{"data":{"createLinkedBranch":{"linkedBranch":{"id":"2","ref":{"name":"my-branch"}}}}}`,
|
||||
func(inputs map[string]interface{}) {
|
||||
assert.Equal(t, "REPOID", inputs["repositoryId"])
|
||||
assert.Equal(t, "SOMEID", inputs["issueId"])
|
||||
assert.Equal(t, "OID", inputs["oid"])
|
||||
assert.Equal(t, "my-branch", inputs["name"])
|
||||
assert.Equal(t, "yar", inputs["issueId"])
|
||||
}),
|
||||
)
|
||||
},
|
||||
runStubs: func(cs *run.CommandStubber) {
|
||||
cs.Register(`git rev-parse --verify refs/heads/my-branch`, 0, "")
|
||||
cs.Register(`git fetch origin \+refs/heads/my-branch:refs/remotes/origin/my-branch`, 0, "")
|
||||
},
|
||||
expectedOut: "github.com/OWNER/REPO/tree/my-branch\n",
|
||||
},
|
||||
{
|
||||
name: "develop new branch outside of local git repo",
|
||||
opts: &DevelopOptions{
|
||||
IssueSelector: "https://github.com/cli/cli/issues/123",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query LinkedBranchFeature\b`),
|
||||
httpmock.StringResponse(featureEnabledPayload),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query IssueByNumber\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id": "SOMEID","number":123,"title":"my issue"}}}}`),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query FindRepoBranchID\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{"id":"REPOID","defaultBranchRef":{"target":{"oid":"DEFAULTOID"}},"ref":{"target":{"oid":""}}}}}`),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`mutation CreateLinkedBranch\b`),
|
||||
httpmock.GraphQLMutation(`{"data":{"createLinkedBranch":{"linkedBranch":{"id":"2","ref":{"name":"my-issue-1"}}}}}`,
|
||||
func(inputs map[string]interface{}) {
|
||||
assert.Equal(t, "REPOID", inputs["repositoryId"])
|
||||
assert.Equal(t, "SOMEID", inputs["issueId"])
|
||||
assert.Equal(t, "DEFAULTOID", inputs["oid"])
|
||||
}),
|
||||
)
|
||||
},
|
||||
expectedOut: "github.com/cli/cli/tree/my-issue-1\n",
|
||||
},
|
||||
{
|
||||
name: "develop new branch with checkout when local branch exists",
|
||||
opts: &DevelopOptions{
|
||||
Name: "my-branch",
|
||||
IssueSelector: "123",
|
||||
Checkout: true,
|
||||
},
|
||||
remotes: map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query LinkedBranchFeature\b`),
|
||||
httpmock.StringResponse(featureEnabledPayload),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query IssueByNumber\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id": "SOMEID","number":123,"title":"my issue"}}}}`),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query FindRepoBranchID\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{"id":"REPOID","ref":{"target":{"oid":"OID"}}}}}`),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`mutation CreateLinkedBranch\b`),
|
||||
httpmock.GraphQLMutation(`{"data":{"createLinkedBranch":{"linkedBranch":{"id":"2","ref":{"name":"my-branch"}}}}}`,
|
||||
func(inputs map[string]interface{}) {
|
||||
assert.Equal(t, "REPOID", inputs["repositoryId"])
|
||||
assert.Equal(t, "SOMEID", inputs["issueId"])
|
||||
assert.Equal(t, "OID", inputs["oid"])
|
||||
assert.Equal(t, "my-branch", inputs["name"])
|
||||
}),
|
||||
)
|
||||
},
|
||||
runStubs: func(cs *run.CommandStubber) {
|
||||
cs.Register(`git fetch origin \+refs/heads/my-branch:refs/remotes/origin/my-branch`, 0, "")
|
||||
cs.Register(`git rev-parse --verify refs/heads/my-branch`, 0, "")
|
||||
cs.Register(`git checkout my-branch`, 0, "")
|
||||
cs.Register(`git pull --ff-only origin my-branch`, 0, "")
|
||||
},
|
||||
expectedOut: "github.com/OWNER/REPO/tree/my-branch\n",
|
||||
},
|
||||
{
|
||||
name: "develop new branch with checkout when the branch does not exist locally",
|
||||
setup: func(opts *DevelopOptions, t *testing.T) func() {
|
||||
opts.Name = "my-branch"
|
||||
opts.BaseBranch = "main"
|
||||
opts.IssueSelector = "123"
|
||||
opts.Checkout = true
|
||||
return func() {}
|
||||
name: "develop new branch with checkout when local branch does not exist",
|
||||
opts: &DevelopOptions{
|
||||
Name: "my-branch",
|
||||
IssueSelector: "123",
|
||||
Checkout: true,
|
||||
},
|
||||
remotes: map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query LinkedBranch_fields\b`),
|
||||
httpmock.GraphQL(`query LinkedBranchFeature\b`),
|
||||
httpmock.StringResponse(featureEnabledPayload),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query RepositoryInfo\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"id": "REPOID",
|
||||
"hasIssuesEnabled": true
|
||||
} } }`),
|
||||
httpmock.GraphQL(`query IssueByNumber\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id": "SOMEID","number":123,"title":"my issue"}}}}`),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query IssueByNumber\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{ "hasIssuesEnabled": true, "issue":{"id": "yar", "number":123, "title":"my issue"} }}}`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query BranchIssueReferenceFindBaseOid\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{"ref":{"target":{"oid":"123"}}}}}`))
|
||||
|
||||
httpmock.GraphQL(`query FindRepoBranchID\b`),
|
||||
httpmock.StringResponse(`{"data":{"repository":{"id":"REPOID","ref":{"target":{"oid":"OID"}}}}}`),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`mutation CreateLinkedBranch\b`),
|
||||
httpmock.GraphQLQuery(`{ "data": { "createLinkedBranch": { "linkedBranch": {"id": "2", "ref": {"name": "my-branch"} } } } }`,
|
||||
func(query string, inputs map[string]interface{}) {
|
||||
httpmock.GraphQLMutation(`{"data":{"createLinkedBranch":{"linkedBranch":{"id":"2","ref":{"name":"my-branch"}}}}}`,
|
||||
func(inputs map[string]interface{}) {
|
||||
assert.Equal(t, "REPOID", inputs["repositoryId"])
|
||||
assert.Equal(t, "SOMEID", inputs["issueId"])
|
||||
assert.Equal(t, "OID", inputs["oid"])
|
||||
assert.Equal(t, "my-branch", inputs["name"])
|
||||
assert.Equal(t, "yar", inputs["issueId"])
|
||||
}),
|
||||
)
|
||||
},
|
||||
runStubs: func(cs *run.CommandStubber) {
|
||||
cs.Register(`git rev-parse --verify refs/heads/my-branch`, 1, "")
|
||||
cs.Register(`git fetch origin \+refs/heads/my-branch:refs/remotes/origin/my-branch`, 0, "")
|
||||
cs.Register(`git rev-parse --verify refs/heads/my-branch`, 1, "")
|
||||
cs.Register(`git checkout -b my-branch --track origin/my-branch`, 0, "")
|
||||
},
|
||||
expectedOut: "github.com/OWNER/REPO/tree/my-branch\n",
|
||||
|
|
@ -546,16 +510,18 @@ func Test_developRun(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
opts := tt.opts
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
if tt.httpStubs != nil {
|
||||
tt.httpStubs(reg, t)
|
||||
}
|
||||
|
||||
opts := DevelopOptions{}
|
||||
opts.HttpClient = func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
|
||||
ios.SetStdoutTTY(tt.tty)
|
||||
ios.SetStdinTTY(tt.tty)
|
||||
ios.SetStderrTTY(tt.tty)
|
||||
|
|
@ -564,12 +530,6 @@ func Test_developRun(t *testing.T) {
|
|||
opts.BaseRepo = func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
}
|
||||
opts.HttpClient = func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
opts.Config = func() (config.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
}
|
||||
|
||||
opts.Remotes = func() (context.Remotes, error) {
|
||||
if len(tt.remotes) == 0 {
|
||||
|
|
@ -600,29 +560,14 @@ func Test_developRun(t *testing.T) {
|
|||
tt.runStubs(cmdStubs)
|
||||
}
|
||||
|
||||
cleanSetup := func() {}
|
||||
if tt.setup != nil {
|
||||
cleanSetup = tt.setup(&opts, t)
|
||||
}
|
||||
defer cleanSetup()
|
||||
|
||||
var err error
|
||||
if opts.List {
|
||||
err = developRunList(&opts)
|
||||
} else {
|
||||
|
||||
err = developRunCreate(&opts)
|
||||
}
|
||||
output := &test.CmdOut{
|
||||
OutBuf: stdout,
|
||||
ErrBuf: stderr,
|
||||
}
|
||||
err := developRun(opts)
|
||||
if tt.wantErr != "" {
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
return
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedOut, output.String())
|
||||
assert.Equal(t, tt.expectedErrOut, output.Stderr())
|
||||
assert.Equal(t, tt.expectedOut, stdout.String())
|
||||
assert.Equal(t, tt.expectedErrOut, stderr.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,10 +21,11 @@ type EditOptions struct {
|
|||
HttpClient func() (*http.Client, error)
|
||||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Prompter prShared.EditPrompter
|
||||
|
||||
DetermineEditor func() (string, error)
|
||||
FieldsToEditSurvey func(*prShared.Editable) error
|
||||
EditFieldsSurvey func(*prShared.Editable, string) error
|
||||
FieldsToEditSurvey func(prShared.EditPrompter, *prShared.Editable) error
|
||||
EditFieldsSurvey func(prShared.EditPrompter, *prShared.Editable, string) error
|
||||
FetchOptions func(*api.Client, ghrepo.Interface, *prShared.Editable) error
|
||||
|
||||
SelectorArgs []string
|
||||
|
|
@ -41,6 +42,7 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
|
|||
FieldsToEditSurvey: prShared.FieldsToEditSurvey,
|
||||
EditFieldsSurvey: prShared.EditFieldsSurvey,
|
||||
FetchOptions: prShared.FetchOptions,
|
||||
Prompter: f.Prompter,
|
||||
}
|
||||
|
||||
var bodyFile string
|
||||
|
|
@ -152,7 +154,7 @@ func editRun(opts *EditOptions) error {
|
|||
// Prompt the user which fields they'd like to edit.
|
||||
editable := opts.Editable
|
||||
if opts.Interactive {
|
||||
err = opts.FieldsToEditSurvey(&editable)
|
||||
err = opts.FieldsToEditSurvey(opts.Prompter, &editable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -222,7 +224,7 @@ func editRun(opts *EditOptions) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = opts.EditFieldsSurvey(&editable, editorCommand)
|
||||
err = opts.EditFieldsSurvey(opts.Prompter, &editable, editorCommand)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -511,7 +511,7 @@ func Test_editRun(t *testing.T) {
|
|||
input: &EditOptions{
|
||||
SelectorArgs: []string{"123"},
|
||||
Interactive: true,
|
||||
FieldsToEditSurvey: func(eo *prShared.Editable) error {
|
||||
FieldsToEditSurvey: func(p prShared.EditPrompter, eo *prShared.Editable) error {
|
||||
eo.Title.Edited = true
|
||||
eo.Body.Edited = true
|
||||
eo.Assignees.Edited = true
|
||||
|
|
@ -520,7 +520,7 @@ func Test_editRun(t *testing.T) {
|
|||
eo.Milestone.Edited = true
|
||||
return nil
|
||||
},
|
||||
EditFieldsSurvey: func(eo *prShared.Editable, _ string) error {
|
||||
EditFieldsSurvey: func(p prShared.EditPrompter, eo *prShared.Editable, _ string) error {
|
||||
eo.Title.Value = "new title"
|
||||
eo.Body.Value = "new body"
|
||||
eo.Assignees.Value = []string{"monalisa", "hubot"}
|
||||
|
|
|
|||
|
|
@ -102,8 +102,8 @@ func createRun(opts *createOptions) error {
|
|||
}
|
||||
|
||||
if opts.Color == "" {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
opts.Color = randomColors[rand.Intn(len(randomColors)-1)]
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
opts.Color = randomColors[r.Intn(len(randomColors)-1)]
|
||||
}
|
||||
|
||||
opts.IO.StartProgressIndicator()
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
# List more organizations
|
||||
$ gh org list --limit 100
|
||||
`),
|
||||
Aliases: []string{"ls"},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if opts.Limit < 1 {
|
||||
return cmdutil.FlagErrorf("invalid limit: %v", opts.Limit)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/browser"
|
||||
fd "github.com/cli/cli/v2/internal/featuredetection"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/cmd/pr/shared"
|
||||
|
|
@ -24,7 +25,8 @@ type ChecksOptions struct {
|
|||
IO *iostreams.IOStreams
|
||||
Browser browser.Browser
|
||||
|
||||
Finder shared.PRFinder
|
||||
Finder shared.PRFinder
|
||||
Detector fd.Detector
|
||||
|
||||
SelectorArg string
|
||||
WebMode bool
|
||||
|
|
@ -142,8 +144,19 @@ func checksRun(opts *ChecksOptions) error {
|
|||
var checks []check
|
||||
var counts checkCounts
|
||||
var err error
|
||||
var includeEvent bool
|
||||
|
||||
checks, counts, err = populateStatusChecks(client, repo, pr, opts.Required)
|
||||
if opts.Detector == nil {
|
||||
cachedClient := api.NewCachedHTTPClient(client, time.Hour*24)
|
||||
opts.Detector = fd.NewDetector(cachedClient, repo.RepoHost())
|
||||
}
|
||||
if features, featuresErr := opts.Detector.PullRequestFeatures(); featuresErr != nil {
|
||||
return featuresErr
|
||||
} else {
|
||||
includeEvent = features.CheckRunEvent
|
||||
}
|
||||
|
||||
checks, counts, err = populateStatusChecks(client, repo, pr, opts.Required, includeEvent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -183,7 +196,7 @@ func checksRun(opts *ChecksOptions) error {
|
|||
|
||||
time.Sleep(opts.Interval)
|
||||
|
||||
checks, counts, err = populateStatusChecks(client, repo, pr, opts.Required)
|
||||
checks, counts, err = populateStatusChecks(client, repo, pr, opts.Required, includeEvent)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
|
@ -203,14 +216,16 @@ func checksRun(opts *ChecksOptions) error {
|
|||
}
|
||||
}
|
||||
|
||||
if counts.Failed+counts.Pending > 0 {
|
||||
if counts.Failed > 0 {
|
||||
return cmdutil.SilentError
|
||||
} else if counts.Pending > 0 {
|
||||
return cmdutil.PendingError
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func populateStatusChecks(client *http.Client, repo ghrepo.Interface, pr *api.PullRequest, requiredChecks bool) ([]check, checkCounts, error) {
|
||||
func populateStatusChecks(client *http.Client, repo ghrepo.Interface, pr *api.PullRequest, requiredChecks bool, includeEvent bool) ([]check, checkCounts, error) {
|
||||
apiClient := api.NewClientFromHTTP(client)
|
||||
|
||||
type response struct {
|
||||
|
|
@ -224,7 +239,7 @@ func populateStatusChecks(client *http.Client, repo ghrepo.Interface, pr *api.Pu
|
|||
%s
|
||||
}
|
||||
}
|
||||
}`, api.RequiredStatusCheckRollupGraphQL("$id", "$endCursor"))
|
||||
}`, api.RequiredStatusCheckRollupGraphQL("$id", "$endCursor", includeEvent))
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"id": pr.ID,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/browser"
|
||||
fd "github.com/cli/cli/v2/internal/featuredetection"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/run"
|
||||
"github.com/cli/cli/v2/pkg/cmd/pr/shared"
|
||||
|
|
@ -123,14 +124,15 @@ func TestNewCmdChecks(t *testing.T) {
|
|||
|
||||
func Test_checksRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tty bool
|
||||
watch bool
|
||||
failFast bool
|
||||
required bool
|
||||
httpStubs func(*httpmock.Registry)
|
||||
wantOut string
|
||||
wantErr string
|
||||
name string
|
||||
tty bool
|
||||
watch bool
|
||||
failFast bool
|
||||
required bool
|
||||
disableDetector bool
|
||||
httpStubs func(*httpmock.Registry)
|
||||
wantOut string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "no commits tty",
|
||||
|
|
@ -178,7 +180,7 @@ func Test_checksRun(t *testing.T) {
|
|||
)
|
||||
},
|
||||
wantOut: "Some checks are still pending\n0 failing, 2 successful, 0 skipped, and 1 pending checks\n\n✓ cool tests 1m26s sweet link\n✓ rad tests 1m26s sweet link\n* slow tests 1m26s sweet link\n",
|
||||
wantErr: "SilentError",
|
||||
wantErr: "PendingError",
|
||||
},
|
||||
{
|
||||
name: "all passing tty",
|
||||
|
|
@ -273,7 +275,7 @@ func Test_checksRun(t *testing.T) {
|
|||
)
|
||||
},
|
||||
wantOut: "cool tests\tpass\t1m26s\tsweet link\t\nrad tests\tpass\t1m26s\tsweet link\t\nslow tests\tpending\t1m26s\tsweet link\t\n",
|
||||
wantErr: "SilentError",
|
||||
wantErr: "PendingError",
|
||||
},
|
||||
{
|
||||
name: "all passing",
|
||||
|
|
@ -393,6 +395,54 @@ func Test_checksRun(t *testing.T) {
|
|||
wantOut: "awesome tests\tpass\t1m26s\tsweet link\tawesome description\ncool tests\tpass\t1m26s\tsweet link\tcool description\nrad tests\tpass\t1m26s\tsweet link\trad description\n",
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "events tty",
|
||||
tty: true,
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query PullRequestStatusChecks\b`),
|
||||
httpmock.FileResponse("./fixtures/withEvents.json"),
|
||||
)
|
||||
},
|
||||
wantOut: "All checks were successful\n0 failing, 2 successful, 0 skipped, and 0 pending checks\n\n✓ tests/cool tests (pull_request) cool description 1m26s sweet link\n✓ tests/cool tests (push) cool description 1m26s sweet link\n",
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "events not supported tty",
|
||||
tty: true,
|
||||
disableDetector: true,
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query PullRequestStatusChecks\b`),
|
||||
httpmock.FileResponse("./fixtures/withoutEvents.json"),
|
||||
)
|
||||
},
|
||||
wantOut: "All checks were successful\n0 failing, 1 successful, 0 skipped, and 0 pending checks\n\n✓ tests/cool tests cool description 1m26s sweet link\n",
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "events",
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query PullRequestStatusChecks\b`),
|
||||
httpmock.FileResponse("./fixtures/withEvents.json"),
|
||||
)
|
||||
},
|
||||
wantOut: "cool tests\tpass\t1m26s\tsweet link\tcool description\ncool tests\tpass\t1m26s\tsweet link\tcool description\n",
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "events not supported",
|
||||
disableDetector: true,
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query PullRequestStatusChecks\b`),
|
||||
httpmock.FileResponse("./fixtures/withoutEvents.json"),
|
||||
)
|
||||
},
|
||||
wantOut: "cool tests\tpass\t1m26s\tsweet link\tcool description\n",
|
||||
wantErr: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -407,7 +457,14 @@ func Test_checksRun(t *testing.T) {
|
|||
tt.httpStubs(reg)
|
||||
}
|
||||
|
||||
var detector fd.Detector
|
||||
detector = &fd.EnabledDetectorMock{}
|
||||
if tt.disableDetector {
|
||||
detector = &fd.DisabledDetectorMock{}
|
||||
}
|
||||
|
||||
response := &api.PullRequest{Number: 123, HeadRefName: "trunk"}
|
||||
|
||||
opts := &ChecksOptions{
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
|
|
@ -415,6 +472,7 @@ func Test_checksRun(t *testing.T) {
|
|||
IO: ios,
|
||||
SelectorArg: "123",
|
||||
Finder: shared.NewMockFinder("123", response, ghrepo.New("OWNER", "REPO")),
|
||||
Detector: detector,
|
||||
Watch: tt.watch,
|
||||
FailFast: tt.failFast,
|
||||
Required: tt.required,
|
||||
|
|
|
|||
55
pkg/cmd/pr/checks/fixtures/withEvents.json
Normal file
55
pkg/cmd/pr/checks/fixtures/withEvents.json
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"data": {
|
||||
"node": {
|
||||
"statusCheckRollup": {
|
||||
"nodes": [
|
||||
{
|
||||
"commit": {
|
||||
"oid": "abc",
|
||||
"statusCheckRollup": {
|
||||
"contexts": {
|
||||
"nodes": [
|
||||
{
|
||||
"conclusion": "SUCCESS",
|
||||
"status": "COMPLETED",
|
||||
"name": "cool tests",
|
||||
"description": "cool description",
|
||||
"completedAt": "2020-08-27T19:00:12Z",
|
||||
"startedAt": "2020-08-27T18:58:46Z",
|
||||
"detailsUrl": "sweet link",
|
||||
"checkSuite": {
|
||||
"workflowRun": {
|
||||
"event": "pull_request",
|
||||
"workflow": {
|
||||
"name": "tests"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"conclusion": "SUCCESS",
|
||||
"status": "COMPLETED",
|
||||
"name": "cool tests",
|
||||
"description": "cool description",
|
||||
"completedAt": "2020-08-27T19:00:12Z",
|
||||
"startedAt": "2020-08-27T18:58:46Z",
|
||||
"detailsUrl": "sweet link",
|
||||
"checkSuite": {
|
||||
"workflowRun": {
|
||||
"event": "push",
|
||||
"workflow": {
|
||||
"name": "tests"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
53
pkg/cmd/pr/checks/fixtures/withoutEvents.json
Normal file
53
pkg/cmd/pr/checks/fixtures/withoutEvents.json
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"data": {
|
||||
"node": {
|
||||
"statusCheckRollup": {
|
||||
"nodes": [
|
||||
{
|
||||
"commit": {
|
||||
"oid": "abc",
|
||||
"statusCheckRollup": {
|
||||
"contexts": {
|
||||
"nodes": [
|
||||
{
|
||||
"conclusion": "SUCCESS",
|
||||
"status": "COMPLETED",
|
||||
"name": "cool tests",
|
||||
"description": "cool description",
|
||||
"completedAt": "2020-08-27T19:00:12Z",
|
||||
"startedAt": "2020-08-27T18:58:46Z",
|
||||
"detailsUrl": "sweet link",
|
||||
"checkSuite": {
|
||||
"workflowRun": {
|
||||
"workflow": {
|
||||
"name": "tests"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"conclusion": "SUCCESS",
|
||||
"status": "COMPLETED",
|
||||
"name": "cool tests",
|
||||
"description": "cool description",
|
||||
"completedAt": "2020-08-27T19:00:12Z",
|
||||
"startedAt": "2020-08-27T18:58:46Z",
|
||||
"detailsUrl": "sweet link",
|
||||
"checkSuite": {
|
||||
"workflowRun": {
|
||||
"workflow": {
|
||||
"name": "tests"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -45,6 +45,7 @@ type CreateOptions struct {
|
|||
RepoOverride string
|
||||
|
||||
Autofill bool
|
||||
FillFirst bool
|
||||
WebMode bool
|
||||
RecoverFile string
|
||||
|
||||
|
|
@ -144,13 +145,19 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
}
|
||||
|
||||
if opts.IsDraft && opts.WebMode {
|
||||
return errors.New("the `--draft` flag is not supported with `--web`")
|
||||
return cmdutil.FlagErrorf("the `--draft` flag is not supported with `--web`")
|
||||
}
|
||||
|
||||
if len(opts.Reviewers) > 0 && opts.WebMode {
|
||||
return errors.New("the `--reviewer` flag is not supported with `--web`")
|
||||
return cmdutil.FlagErrorf("the `--reviewer` flag is not supported with `--web`")
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("no-maintainer-edit") && opts.WebMode {
|
||||
return errors.New("the `--no-maintainer-edit` flag is not supported with `--web`")
|
||||
return cmdutil.FlagErrorf("the `--no-maintainer-edit` flag is not supported with `--web`")
|
||||
}
|
||||
|
||||
if opts.Autofill && opts.FillFirst {
|
||||
return cmdutil.FlagErrorf("`--fill` is not supported with `--fill-first`")
|
||||
}
|
||||
|
||||
opts.BodyProvided = cmd.Flags().Changed("body")
|
||||
|
|
@ -164,11 +171,11 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
}
|
||||
|
||||
if opts.Template != "" && opts.BodyProvided {
|
||||
return errors.New("`--template` is not supported when using `--body` or `--body-file`")
|
||||
return cmdutil.FlagErrorf("`--template` is not supported when using `--body` or `--body-file`")
|
||||
}
|
||||
|
||||
if !opts.IO.CanPrompt() && !opts.WebMode && !opts.Autofill && (!opts.TitleProvided || !opts.BodyProvided) {
|
||||
return cmdutil.FlagErrorf("must provide `--title` and `--body` (or `--fill`) when not running interactively")
|
||||
if !opts.IO.CanPrompt() && !opts.WebMode && !(opts.Autofill || opts.FillFirst) && (!opts.TitleProvided || !opts.BodyProvided) {
|
||||
return cmdutil.FlagErrorf("must provide `--title` and `--body` (or `--fill` or `fill-first`) when not running interactively")
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
|
|
@ -187,6 +194,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
fl.StringVarP(&opts.HeadBranch, "head", "H", "", "The `branch` that contains commits for your pull request (default: current branch)")
|
||||
fl.BoolVarP(&opts.WebMode, "web", "w", false, "Open the web browser to create a pull request")
|
||||
fl.BoolVarP(&opts.Autofill, "fill", "f", false, "Do not prompt for title/body and just use commit info")
|
||||
fl.BoolVar(&opts.FillFirst, "fill-first", false, "Do not prompt for title/body and just use first commit info")
|
||||
fl.StringSliceVarP(&opts.Reviewers, "reviewer", "r", nil, "Request reviews from people or teams by their `handle`")
|
||||
fl.StringSliceVarP(&opts.Assignees, "assignee", "a", nil, "Assign people by their `login`. Use \"@me\" to self-assign.")
|
||||
fl.StringSliceVarP(&opts.Labels, "label", "l", nil, "Add labels by `name`")
|
||||
|
|
@ -225,7 +233,7 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
var openURL string
|
||||
|
||||
if opts.WebMode {
|
||||
if !opts.Autofill {
|
||||
if !(opts.Autofill || opts.FillFirst) {
|
||||
state.Title = opts.Title
|
||||
state.Body = opts.Body
|
||||
}
|
||||
|
|
@ -281,7 +289,7 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
ghrepo.FullName(ctx.BaseRepo))
|
||||
}
|
||||
|
||||
if opts.Autofill || (opts.TitleProvided && opts.BodyProvided) {
|
||||
if opts.Autofill || opts.FillFirst || (opts.TitleProvided && opts.BodyProvided) {
|
||||
err = handlePush(*opts, *ctx)
|
||||
if err != nil {
|
||||
return
|
||||
|
|
@ -353,7 +361,7 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
Repo: ctx.BaseRepo,
|
||||
State: state,
|
||||
}
|
||||
err = shared.MetadataSurvey(opts.IO, ctx.BaseRepo, fetcher, state)
|
||||
err = shared.MetadataSurvey(opts.Prompter, opts.IO, ctx.BaseRepo, fetcher, state)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -392,7 +400,7 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func initDefaultTitleBody(ctx CreateContext, state *shared.IssueMetadataState) error {
|
||||
func initDefaultTitleBody(ctx CreateContext, state *shared.IssueMetadataState, useFirstCommit bool) error {
|
||||
baseRef := ctx.BaseTrackingBranch
|
||||
headRef := ctx.HeadBranch
|
||||
gitClient := ctx.GitClient
|
||||
|
|
@ -401,17 +409,16 @@ func initDefaultTitleBody(ctx CreateContext, state *shared.IssueMetadataState) e
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(commits) == 1 {
|
||||
state.Title = commits[0].Title
|
||||
body, err := gitClient.CommitBody(context.Background(), commits[0].Sha)
|
||||
if len(commits) == 1 || useFirstCommit {
|
||||
commitIndex := len(commits) - 1
|
||||
state.Title = commits[commitIndex].Title
|
||||
body, err := gitClient.CommitBody(context.Background(), commits[commitIndex].Sha)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state.Body = body
|
||||
} else {
|
||||
state.Title = humanize(headRef)
|
||||
|
||||
var body strings.Builder
|
||||
for i := len(commits) - 1; i >= 0; i-- {
|
||||
fmt.Fprintf(&body, "- %s\n", commits[i].Title)
|
||||
|
|
@ -485,9 +492,9 @@ func NewIssueState(ctx CreateContext, opts CreateOptions) (*shared.IssueMetadata
|
|||
Draft: opts.IsDraft,
|
||||
}
|
||||
|
||||
if opts.Autofill || !opts.TitleProvided || !opts.BodyProvided {
|
||||
err := initDefaultTitleBody(ctx, state)
|
||||
if err != nil && opts.Autofill {
|
||||
if opts.Autofill || opts.FillFirst || !opts.TitleProvided || !opts.BodyProvided {
|
||||
err := initDefaultTitleBody(ctx, state, opts.FillFirst)
|
||||
if err != nil && (opts.Autofill || opts.FillFirst) {
|
||||
return nil, fmt.Errorf("could not compute title or body defaults: %w", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -168,6 +168,32 @@ func TestNewCmdCreate(t *testing.T) {
|
|||
cli: "-t mytitle --template bug_fix.md --body-file body_file.md",
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "fill-first",
|
||||
tty: false,
|
||||
cli: "--fill-first",
|
||||
wantsErr: false,
|
||||
wantsOpts: CreateOptions{
|
||||
Title: "",
|
||||
TitleProvided: false,
|
||||
Body: "",
|
||||
BodyProvided: false,
|
||||
Autofill: false,
|
||||
FillFirst: true,
|
||||
RecoverFile: "",
|
||||
WebMode: false,
|
||||
IsDraft: false,
|
||||
BaseBranch: "",
|
||||
HeadBranch: "",
|
||||
MaintainerCanModify: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fill and fill-first",
|
||||
tty: false,
|
||||
cli: "--fill --fill-first",
|
||||
wantsErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
@ -210,6 +236,7 @@ func TestNewCmdCreate(t *testing.T) {
|
|||
assert.Equal(t, tt.wantsOpts.Title, opts.Title)
|
||||
assert.Equal(t, tt.wantsOpts.TitleProvided, opts.TitleProvided)
|
||||
assert.Equal(t, tt.wantsOpts.Autofill, opts.Autofill)
|
||||
assert.Equal(t, tt.wantsOpts.FillFirst, opts.FillFirst)
|
||||
assert.Equal(t, tt.wantsOpts.WebMode, opts.WebMode)
|
||||
assert.Equal(t, tt.wantsOpts.RecoverFile, opts.RecoverFile)
|
||||
assert.Equal(t, tt.wantsOpts.IsDraft, opts.IsDraft)
|
||||
|
|
@ -969,6 +996,47 @@ func Test_createRun(t *testing.T) {
|
|||
},
|
||||
expectedOut: "https://github.com/OWNER/REPO/pull/12\n",
|
||||
},
|
||||
{
|
||||
name: "fill-first flag provided",
|
||||
tty: true,
|
||||
setup: func(opts *CreateOptions, t *testing.T) func() {
|
||||
opts.FillFirst = true
|
||||
opts.HeadBranch = "feature"
|
||||
return func() {}
|
||||
},
|
||||
cmdStubs: func(cs *run.CommandStubber) {
|
||||
cs.Register(
|
||||
"git -c log.ShowSignature=false log --pretty=format:%H,%s --cherry origin/master...feature",
|
||||
0,
|
||||
"56b6f8bb7c9e3a30093cd17e48934ce354148e80,second commit of pr\n"+
|
||||
"343jdfe47c9e3a30093cd17e48934ce354148e80,first commit of pr",
|
||||
)
|
||||
cs.Register(
|
||||
"git -c log.ShowSignature=false show -s --pretty=format:%b 343jdfe47c9e3a30093cd17e48934ce354148e80",
|
||||
0,
|
||||
"first commit description",
|
||||
)
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry, t *testing.T) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`mutation PullRequestCreate\b`),
|
||||
httpmock.GraphQLMutation(`
|
||||
{
|
||||
"data": { "createPullRequest": { "pullRequest": {
|
||||
"URL": "https://github.com/OWNER/REPO/pull/12"
|
||||
} } }
|
||||
}
|
||||
`,
|
||||
func(input map[string]interface{}) {
|
||||
assert.Equal(t, "first commit of pr", input["title"], "pr title should be first commit message")
|
||||
assert.Equal(t, "first commit description", input["body"], "pr body should be first commit description")
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
expectedOut: "https://github.com/OWNER/REPO/pull/12\n",
|
||||
expectedErrOut: "\nCreating pull request for feature into master in OWNER/REPO\n\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ type EditOptions struct {
|
|||
Surveyor Surveyor
|
||||
Fetcher EditableOptionsFetcher
|
||||
EditorRetriever EditorRetriever
|
||||
Prompter shared.EditPrompter
|
||||
|
||||
SelectorArg string
|
||||
Interactive bool
|
||||
|
|
@ -35,9 +36,10 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
|
|||
opts := &EditOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
Surveyor: surveyor{},
|
||||
Surveyor: surveyor{P: f.Prompter},
|
||||
Fetcher: fetcher{},
|
||||
EditorRetriever: editorRetriever{config: f.Config},
|
||||
Prompter: f.Prompter,
|
||||
}
|
||||
|
||||
var bodyFile string
|
||||
|
|
@ -260,6 +262,10 @@ func updatePullRequestReviews(httpClient *http.Client, repo ghrepo.Interface, id
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if (userIds == nil || len(*userIds) == 0) &&
|
||||
(teamIds == nil || len(*teamIds) == 0) {
|
||||
return nil
|
||||
}
|
||||
union := githubv4.Boolean(false)
|
||||
reviewsRequestParams := githubv4.RequestReviewsInput{
|
||||
PullRequestID: id,
|
||||
|
|
@ -276,14 +282,16 @@ type Surveyor interface {
|
|||
EditFields(*shared.Editable, string) error
|
||||
}
|
||||
|
||||
type surveyor struct{}
|
||||
type surveyor struct {
|
||||
P shared.EditPrompter
|
||||
}
|
||||
|
||||
func (s surveyor) FieldsToEdit(editable *shared.Editable) error {
|
||||
return shared.FieldsToEditSurvey(editable)
|
||||
return shared.FieldsToEditSurvey(s.P, editable)
|
||||
}
|
||||
|
||||
func (s surveyor) EditFields(editable *shared.Editable, editorCmd string) error {
|
||||
return shared.EditFieldsSurvey(editable, editorCmd)
|
||||
return shared.EditFieldsSurvey(s.P, editable, editorCmd)
|
||||
}
|
||||
|
||||
type EditableOptionsFetcher interface {
|
||||
|
|
|
|||
|
|
@ -199,7 +199,6 @@ type mergeContext struct {
|
|||
autoMerge bool
|
||||
crossRepoPR bool
|
||||
deleteBranch bool
|
||||
switchedToBranch string
|
||||
mergeQueueRequired bool
|
||||
}
|
||||
|
||||
|
|
@ -370,7 +369,7 @@ func (m *mergeContext) merge() error {
|
|||
|
||||
// Delete local branch if requested and if allowed.
|
||||
func (m *mergeContext) deleteLocalBranch() error {
|
||||
if m.crossRepoPR || m.autoMerge {
|
||||
if m.autoMerge {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -395,6 +394,8 @@ func (m *mergeContext) deleteLocalBranch() error {
|
|||
return err
|
||||
}
|
||||
|
||||
switchedToBranch := ""
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// branch the command was run on is the same as the pull request branch
|
||||
|
|
@ -424,14 +425,19 @@ func (m *mergeContext) deleteLocalBranch() error {
|
|||
_ = m.warnf(fmt.Sprintf("%s warning: not possible to fast-forward to: %q\n", m.cs.WarningIcon(), targetBranch))
|
||||
}
|
||||
|
||||
m.switchedToBranch = targetBranch
|
||||
switchedToBranch = targetBranch
|
||||
}
|
||||
|
||||
if err := m.opts.GitClient.DeleteLocalBranch(ctx, m.pr.HeadRefName); err != nil {
|
||||
return fmt.Errorf("failed to delete local branch %s: %w", m.cs.Cyan(m.pr.HeadRefName), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
switchedStatement := ""
|
||||
if switchedToBranch != "" {
|
||||
switchedStatement = fmt.Sprintf(" and switched to branch %s", m.cs.Cyan(switchedToBranch))
|
||||
}
|
||||
|
||||
return m.infof("%s Deleted local branch %s%s\n", m.cs.SuccessIconWithColor(m.cs.Red), m.cs.Cyan(m.pr.HeadRefName), switchedStatement)
|
||||
}
|
||||
|
||||
// Delete the remote branch if requested and if allowed.
|
||||
|
|
@ -451,11 +457,7 @@ func (m *mergeContext) deleteRemoteBranch() error {
|
|||
}
|
||||
}
|
||||
|
||||
branch := ""
|
||||
if m.switchedToBranch != "" {
|
||||
branch = fmt.Sprintf(" and switched to branch %s", m.cs.Cyan(m.switchedToBranch))
|
||||
}
|
||||
return m.infof("%s Deleted branch %s%s\n", m.cs.SuccessIconWithColor(m.cs.Red), m.cs.Cyan(m.pr.HeadRefName), branch)
|
||||
return m.infof("%s Deleted remote branch %s\n", m.cs.SuccessIconWithColor(m.cs.Red), m.cs.Cyan(m.pr.HeadRefName))
|
||||
}
|
||||
|
||||
// Add the Pull Request to a merge queue
|
||||
|
|
@ -577,11 +579,16 @@ func mergeMethodSurvey(p shared.Prompt, baseRepo *api.Repository) (PullRequestMe
|
|||
}
|
||||
|
||||
func deleteBranchSurvey(opts *MergeOptions, crossRepoPR, localBranchExists bool) (bool, error) {
|
||||
if !crossRepoPR && !opts.IsDeleteBranchIndicated {
|
||||
if !opts.IsDeleteBranchIndicated {
|
||||
var message string
|
||||
|
||||
if opts.CanDeleteLocalBranch && localBranchExists {
|
||||
message = "Delete the branch locally and on GitHub?"
|
||||
} else {
|
||||
if crossRepoPR {
|
||||
message = "Delete the branch locally?"
|
||||
} else {
|
||||
message = "Delete the branch locally and on GitHub?"
|
||||
}
|
||||
} else if !crossRepoPR {
|
||||
message = "Delete the branch on GitHub?"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -654,7 +654,8 @@ func TestPrMerge_deleteBranch(t *testing.T) {
|
|||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, heredoc.Doc(`
|
||||
✓ Merged pull request #10 (Blueberries are a good fruit)
|
||||
✓ Deleted branch blueberries and switched to branch main
|
||||
✓ Deleted local branch blueberries and switched to branch main
|
||||
✓ Deleted remote branch blueberries
|
||||
`), output.Stderr())
|
||||
}
|
||||
|
||||
|
|
@ -704,7 +705,56 @@ func TestPrMerge_deleteBranch_nonDefault(t *testing.T) {
|
|||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, heredoc.Doc(`
|
||||
✓ Merged pull request #10 (Blueberries are a good fruit)
|
||||
✓ Deleted branch blueberries and switched to branch fruit
|
||||
✓ Deleted local branch blueberries and switched to branch fruit
|
||||
✓ Deleted remote branch blueberries
|
||||
`), output.Stderr())
|
||||
}
|
||||
|
||||
func TestPrMerge_deleteBranch_onlyLocally(t *testing.T) {
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
|
||||
shared.RunCommandFinder(
|
||||
"",
|
||||
&api.PullRequest{
|
||||
ID: "PR_10",
|
||||
Number: 10,
|
||||
State: "OPEN",
|
||||
Title: "Blueberries are a good fruit",
|
||||
HeadRefName: "blueberries",
|
||||
BaseRefName: "main",
|
||||
MergeStateStatus: "CLEAN",
|
||||
HeadRepositoryOwner: api.Owner{Login: "HEAD"}, // Not the same owner as the base repo
|
||||
},
|
||||
baseRepo("OWNER", "REPO", "main"),
|
||||
)
|
||||
|
||||
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 rev-parse --verify refs/heads/main`, 0, "")
|
||||
cs.Register(`git checkout main`, 0, "")
|
||||
cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "")
|
||||
cs.Register(`git branch -D blueberries`, 0, "")
|
||||
cs.Register(`git pull --ff-only`, 0, "")
|
||||
|
||||
output, err := runCommand(http, nil, "blueberries", true, `pr merge --merge --delete-branch`)
|
||||
if err != nil {
|
||||
t.Fatalf("Got unexpected error running `pr merge` %s", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, heredoc.Doc(`
|
||||
✓ Merged pull request #10 (Blueberries are a good fruit)
|
||||
✓ Deleted local branch blueberries and switched to branch main
|
||||
`), output.Stderr())
|
||||
}
|
||||
|
||||
|
|
@ -754,7 +804,8 @@ func TestPrMerge_deleteBranch_checkoutNewBranch(t *testing.T) {
|
|||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, heredoc.Doc(`
|
||||
✓ Merged pull request #10 (Blueberries are a good fruit)
|
||||
✓ Deleted branch blueberries and switched to branch fruit
|
||||
✓ Deleted local branch blueberries and switched to branch fruit
|
||||
✓ Deleted remote branch blueberries
|
||||
`), output.Stderr())
|
||||
}
|
||||
|
||||
|
|
@ -800,7 +851,8 @@ func TestPrMerge_deleteNonCurrentBranch(t *testing.T) {
|
|||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, heredoc.Doc(`
|
||||
✓ Merged pull request #10 (Blueberries are a good fruit)
|
||||
✓ Deleted branch blueberries
|
||||
✓ Deleted local branch blueberries
|
||||
✓ Deleted remote branch blueberries
|
||||
`), output.Stderr())
|
||||
}
|
||||
|
||||
|
|
@ -1044,7 +1096,10 @@ func TestPrMerge_alreadyMerged(t *testing.T) {
|
|||
output, err := runCommand(http, pm, "blueberries", true, "pr merge 4")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "✓ Deleted branch blueberries and switched to branch main\n", output.Stderr())
|
||||
assert.Equal(t, heredoc.Doc(`
|
||||
✓ Deleted local branch blueberries and switched to branch main
|
||||
✓ Deleted remote branch blueberries
|
||||
`), output.Stderr())
|
||||
}
|
||||
|
||||
func TestPrMerge_alreadyMerged_withMergeStrategy(t *testing.T) {
|
||||
|
|
@ -1115,7 +1170,7 @@ func TestPrMerge_alreadyMerged_withMergeStrategy_TTY(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "✓ Deleted branch \n", output.Stderr())
|
||||
assert.Equal(t, "✓ Deleted local branch \n✓ Deleted remote branch \n", output.Stderr())
|
||||
}
|
||||
|
||||
func TestPrMerge_alreadyMerged_withMergeStrategy_crossRepo(t *testing.T) {
|
||||
|
|
@ -1139,7 +1194,17 @@ func TestPrMerge_alreadyMerged_withMergeStrategy_crossRepo(t *testing.T) {
|
|||
|
||||
cs.Register(`git rev-parse --verify refs/heads/`, 0, "")
|
||||
|
||||
output, err := runCommand(http, nil, "blueberries", true, "pr merge 4 --merge")
|
||||
pm := &prompter.PrompterMock{
|
||||
ConfirmFunc: func(p string, d bool) (bool, error) {
|
||||
if p == "Pull request #4 was already merged. Delete the branch locally?" {
|
||||
return d, nil
|
||||
} else {
|
||||
return false, prompter.NoSuchPromptErr(p)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
output, err := runCommand(http, pm, "blueberries", true, "pr merge 4 --merge")
|
||||
if err != nil {
|
||||
t.Fatalf("Got unexpected error running `pr merge` %s", err)
|
||||
}
|
||||
|
|
@ -1282,7 +1347,8 @@ func TestPRMergeTTY_withDeleteBranch(t *testing.T) {
|
|||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, heredoc.Doc(`
|
||||
✓ Merged pull request #3 (It was the best of times)
|
||||
✓ Deleted branch blueberries and switched to branch main
|
||||
✓ Deleted local branch blueberries and switched to branch main
|
||||
✓ Deleted remote branch blueberries
|
||||
`), output.Stderr())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,9 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/pkg/set"
|
||||
"github.com/cli/cli/v2/pkg/surveyext"
|
||||
)
|
||||
|
||||
type Editable struct {
|
||||
|
|
@ -255,34 +252,45 @@ func (ep *EditableProjects) clone() EditableProjects {
|
|||
}
|
||||
}
|
||||
|
||||
func EditFieldsSurvey(editable *Editable, editorCommand string) error {
|
||||
type EditPrompter interface {
|
||||
Select(string, string, []string) (int, error)
|
||||
Input(string, string) (string, error)
|
||||
MarkdownEditor(string, string, bool) (string, error)
|
||||
MultiSelect(string, []string, []string) ([]int, error)
|
||||
Confirm(string, bool) (bool, error)
|
||||
}
|
||||
|
||||
func EditFieldsSurvey(p EditPrompter, editable *Editable, editorCommand string) error {
|
||||
var err error
|
||||
if editable.Title.Edited {
|
||||
editable.Title.Value, err = titleSurvey(editable.Title.Default)
|
||||
editable.Title.Value, err = p.Input("Title", editable.Title.Default)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if editable.Body.Edited {
|
||||
editable.Body.Value, err = bodySurvey(editable.Body.Default, editorCommand)
|
||||
editable.Body.Value, err = p.MarkdownEditor("Body", editable.Body.Default, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if editable.Reviewers.Edited {
|
||||
editable.Reviewers.Value, err = multiSelectSurvey("Reviewers", editable.Reviewers.Default, editable.Reviewers.Options)
|
||||
editable.Reviewers.Value, err = multiSelectSurvey(
|
||||
p, "Reviewers", editable.Reviewers.Default, editable.Reviewers.Options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if editable.Assignees.Edited {
|
||||
editable.Assignees.Value, err = multiSelectSurvey("Assignees", editable.Assignees.Default, editable.Assignees.Options)
|
||||
editable.Assignees.Value, err = multiSelectSurvey(
|
||||
p, "Assignees", editable.Assignees.Default, editable.Assignees.Options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if editable.Labels.Edited {
|
||||
editable.Labels.Add, err = multiSelectSurvey("Labels", editable.Labels.Default, editable.Labels.Options)
|
||||
editable.Labels.Add, err = multiSelectSurvey(
|
||||
p, "Labels", editable.Labels.Default, editable.Labels.Options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -300,18 +308,19 @@ func EditFieldsSurvey(editable *Editable, editorCommand string) error {
|
|||
}
|
||||
}
|
||||
if editable.Projects.Edited {
|
||||
editable.Projects.Value, err = multiSelectSurvey("Projects", editable.Projects.Default, editable.Projects.Options)
|
||||
editable.Projects.Value, err = multiSelectSurvey(
|
||||
p, "Projects", editable.Projects.Default, editable.Projects.Options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if editable.Milestone.Edited {
|
||||
editable.Milestone.Value, err = milestoneSurvey(editable.Milestone.Default, editable.Milestone.Options)
|
||||
editable.Milestone.Value, err = milestoneSurvey(p, editable.Milestone.Default, editable.Milestone.Options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
confirm, err := confirmSurvey()
|
||||
confirm, err := p.Confirm("Submit?", true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -322,7 +331,7 @@ func EditFieldsSurvey(editable *Editable, editorCommand string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func FieldsToEditSurvey(editable *Editable) error {
|
||||
func FieldsToEditSurvey(p EditPrompter, editable *Editable) error {
|
||||
contains := func(s []string, str string) bool {
|
||||
for _, v := range s {
|
||||
if v == str {
|
||||
|
|
@ -337,7 +346,7 @@ func FieldsToEditSurvey(editable *Editable) error {
|
|||
opts = append(opts, "Reviewers")
|
||||
}
|
||||
opts = append(opts, "Assignees", "Labels", "Projects", "Milestone")
|
||||
results, err := multiSelectSurvey("What would you like to edit?", []string{}, opts)
|
||||
results, err := multiSelectSurvey(p, "What would you like to edit?", []string{}, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -414,67 +423,34 @@ func FetchOptions(client *api.Client, repo ghrepo.Interface, editable *Editable)
|
|||
return nil
|
||||
}
|
||||
|
||||
func titleSurvey(title string) (string, error) {
|
||||
var result string
|
||||
q := &survey.Input{
|
||||
Message: "Title",
|
||||
Default: title,
|
||||
}
|
||||
err := survey.AskOne(q, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func bodySurvey(body, editorCommand string) (string, error) {
|
||||
var result string
|
||||
q := &surveyext.GhEditor{
|
||||
EditorCommand: editorCommand,
|
||||
Editor: &survey.Editor{
|
||||
Message: "Body",
|
||||
FileName: "*.md",
|
||||
Default: body,
|
||||
HideDefault: true,
|
||||
AppendDefault: true,
|
||||
},
|
||||
}
|
||||
err := survey.AskOne(q, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func multiSelectSurvey(message string, defaults, options []string) ([]string, error) {
|
||||
func multiSelectSurvey(p EditPrompter, message string, defaults, options []string) (results []string, err error) {
|
||||
if len(options) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var results []string
|
||||
q := &survey.MultiSelect{
|
||||
Message: message,
|
||||
Options: options,
|
||||
Default: defaults,
|
||||
Filter: prompter.LatinMatchingFilter,
|
||||
|
||||
var selected []int
|
||||
selected, err = p.MultiSelect(message, defaults, options)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err := survey.AskOne(q, &results)
|
||||
|
||||
for _, i := range selected {
|
||||
results = append(results, options[i])
|
||||
}
|
||||
|
||||
return results, err
|
||||
}
|
||||
|
||||
func milestoneSurvey(title string, opts []string) (string, error) {
|
||||
func milestoneSurvey(p EditPrompter, title string, opts []string) (result string, err error) {
|
||||
if len(opts) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
var result string
|
||||
q := &survey.Select{
|
||||
Message: "Milestone",
|
||||
Options: opts,
|
||||
Default: title,
|
||||
var selected int
|
||||
selected, err = p.Select("Milestone", title, opts)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err := survey.AskOne(q, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func confirmSurvey() (bool, error) {
|
||||
var result bool
|
||||
q := &survey.Confirm{
|
||||
Message: "Submit?",
|
||||
Default: true,
|
||||
}
|
||||
err := survey.AskOne(q, &result)
|
||||
return result, err
|
||||
result = opts[selected]
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,9 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/pkg/prompt"
|
||||
)
|
||||
|
||||
type Action int
|
||||
|
|
@ -37,6 +34,7 @@ type Prompt interface {
|
|||
Select(string, string, []string) (int, error)
|
||||
MarkdownEditor(string, string, bool) (string, error)
|
||||
Confirm(string, bool) (bool, error)
|
||||
MultiSelect(string, []string, []string) ([]int, error)
|
||||
}
|
||||
|
||||
func ConfirmIssueSubmission(p Prompt, allowPreview bool, allowMetadata bool) (Action, error) {
|
||||
|
|
@ -142,7 +140,7 @@ type RepoMetadataFetcher interface {
|
|||
RepoMetadataFetch(api.RepoMetadataInput) (*api.RepoMetadataResult, error)
|
||||
}
|
||||
|
||||
func MetadataSurvey(io *iostreams.IOStreams, baseRepo ghrepo.Interface, fetcher RepoMetadataFetcher, state *IssueMetadataState) error {
|
||||
func MetadataSurvey(p Prompt, io *iostreams.IOStreams, baseRepo ghrepo.Interface, fetcher RepoMetadataFetcher, state *IssueMetadataState) error {
|
||||
isChosen := func(m string) bool {
|
||||
for _, c := range state.Metadata {
|
||||
if m == c {
|
||||
|
|
@ -160,18 +158,12 @@ func MetadataSurvey(io *iostreams.IOStreams, baseRepo ghrepo.Interface, fetcher
|
|||
}
|
||||
extraFieldsOptions = append(extraFieldsOptions, "Assignees", "Labels", "Projects", "Milestone")
|
||||
|
||||
//nolint:staticcheck // SA1019: prompt.SurveyAsk is deprecated: use Prompter
|
||||
err := prompt.SurveyAsk([]*survey.Question{
|
||||
{
|
||||
Name: "metadata",
|
||||
Prompt: &survey.MultiSelect{
|
||||
Message: "What would you like to add?",
|
||||
Options: extraFieldsOptions,
|
||||
},
|
||||
},
|
||||
}, state)
|
||||
selected, err := p.MultiSelect("What would you like to add?", nil, extraFieldsOptions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not prompt: %w", err)
|
||||
return err
|
||||
}
|
||||
for _, i := range selected {
|
||||
state.Metadata = append(state.Metadata, extraFieldsOptions[i])
|
||||
}
|
||||
|
||||
metadataInput := api.RepoMetadataInput{
|
||||
|
|
@ -215,63 +207,62 @@ func MetadataSurvey(io *iostreams.IOStreams, baseRepo ghrepo.Interface, fetcher
|
|||
milestones = append(milestones, m.Title)
|
||||
}
|
||||
|
||||
var mqs []*survey.Question
|
||||
values := struct {
|
||||
Reviewers []string
|
||||
Assignees []string
|
||||
Labels []string
|
||||
Projects []string
|
||||
Milestone string
|
||||
}{}
|
||||
|
||||
if isChosen("Reviewers") {
|
||||
if len(reviewers) > 0 {
|
||||
mqs = append(mqs, &survey.Question{
|
||||
Name: "reviewers",
|
||||
Prompt: &survey.MultiSelect{
|
||||
Message: "Reviewers",
|
||||
Options: reviewers,
|
||||
Default: state.Reviewers,
|
||||
Filter: prompter.LatinMatchingFilter,
|
||||
},
|
||||
})
|
||||
selected, err := p.MultiSelect("Reviewers", state.Reviewers, reviewers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, i := range selected {
|
||||
values.Reviewers = append(values.Reviewers, reviewers[i])
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintln(io.ErrOut, "warning: no available reviewers")
|
||||
}
|
||||
}
|
||||
if isChosen("Assignees") {
|
||||
if len(assignees) > 0 {
|
||||
mqs = append(mqs, &survey.Question{
|
||||
Name: "assignees",
|
||||
Prompt: &survey.MultiSelect{
|
||||
Message: "Assignees",
|
||||
Options: assignees,
|
||||
Default: state.Assignees,
|
||||
Filter: prompter.LatinMatchingFilter,
|
||||
},
|
||||
})
|
||||
selected, err := p.MultiSelect("Assignees", state.Assignees, assignees)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, i := range selected {
|
||||
values.Assignees = append(values.Assignees, assignees[i])
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintln(io.ErrOut, "warning: no assignable users")
|
||||
}
|
||||
}
|
||||
if isChosen("Labels") {
|
||||
if len(labels) > 0 {
|
||||
mqs = append(mqs, &survey.Question{
|
||||
Name: "labels",
|
||||
Prompt: &survey.MultiSelect{
|
||||
Message: "Labels",
|
||||
Options: labels,
|
||||
Default: state.Labels,
|
||||
Filter: prompter.LatinMatchingFilter,
|
||||
},
|
||||
})
|
||||
selected, err := p.MultiSelect("Labels", state.Labels, labels)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, i := range selected {
|
||||
values.Labels = append(values.Labels, labels[i])
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintln(io.ErrOut, "warning: no labels in the repository")
|
||||
}
|
||||
}
|
||||
if isChosen("Projects") {
|
||||
if len(projects) > 0 {
|
||||
mqs = append(mqs, &survey.Question{
|
||||
Name: "projects",
|
||||
Prompt: &survey.MultiSelect{
|
||||
Message: "Projects",
|
||||
Options: projects,
|
||||
Default: state.Projects,
|
||||
Filter: prompter.LatinMatchingFilter,
|
||||
},
|
||||
})
|
||||
selected, err := p.MultiSelect("Projects", state.Projects, projects)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, i := range selected {
|
||||
values.Projects = append(values.Projects, projects[i])
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintln(io.ErrOut, "warning: no projects to choose from")
|
||||
}
|
||||
|
|
@ -284,33 +275,16 @@ func MetadataSurvey(io *iostreams.IOStreams, baseRepo ghrepo.Interface, fetcher
|
|||
} else {
|
||||
milestoneDefault = milestones[1]
|
||||
}
|
||||
mqs = append(mqs, &survey.Question{
|
||||
Name: "milestone",
|
||||
Prompt: &survey.Select{
|
||||
Message: "Milestone",
|
||||
Options: milestones,
|
||||
Default: milestoneDefault,
|
||||
},
|
||||
})
|
||||
selected, err := p.Select("Milestone", milestoneDefault, milestones)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
values.Milestone = milestones[selected]
|
||||
} else {
|
||||
fmt.Fprintln(io.ErrOut, "warning: no milestones in the repository")
|
||||
}
|
||||
}
|
||||
|
||||
values := struct {
|
||||
Reviewers []string
|
||||
Assignees []string
|
||||
Labels []string
|
||||
Projects []string
|
||||
Milestone string
|
||||
}{}
|
||||
|
||||
//nolint:staticcheck // SA1019: prompt.SurveyAsk is deprecated: use Prompter
|
||||
err = prompt.SurveyAsk(mqs, &values)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
|
||||
if isChosen("Reviewers") {
|
||||
var logins []string
|
||||
for _, r := range values.Reviewers {
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import (
|
|||
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/pkg/prompt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
@ -43,45 +43,32 @@ func TestMetadataSurvey_selectAll(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
//nolint:staticcheck // SA1019: prompt.InitAskStubber is deprecated: use NewAskStubber
|
||||
as, restoreAsk := prompt.InitAskStubber()
|
||||
defer restoreAsk()
|
||||
|
||||
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
|
||||
as.Stub([]*prompt.QuestionStub{
|
||||
{
|
||||
Name: "metadata",
|
||||
Value: []string{"Labels", "Projects", "Assignees", "Reviewers", "Milestone"},
|
||||
},
|
||||
pm := prompter.NewMockPrompter(t)
|
||||
pm.RegisterMultiSelect("What would you like to add?",
|
||||
[]string{}, []string{"Reviewers", "Assignees", "Labels", "Projects", "Milestone"}, func(_ string, _, _ []string) ([]int, error) {
|
||||
return []int{0, 1, 2, 3, 4}, nil
|
||||
})
|
||||
pm.RegisterMultiSelect("Reviewers", []string{}, []string{"hubot", "monalisa"}, func(_ string, _, _ []string) ([]int, error) {
|
||||
return []int{1}, nil
|
||||
})
|
||||
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
|
||||
as.Stub([]*prompt.QuestionStub{
|
||||
{
|
||||
Name: "reviewers",
|
||||
Value: []string{"monalisa"},
|
||||
},
|
||||
{
|
||||
Name: "assignees",
|
||||
Value: []string{"hubot"},
|
||||
},
|
||||
{
|
||||
Name: "labels",
|
||||
Value: []string{"good first issue"},
|
||||
},
|
||||
{
|
||||
Name: "projects",
|
||||
Value: []string{"The road to 1.0"},
|
||||
},
|
||||
{
|
||||
Name: "milestone",
|
||||
Value: "(none)",
|
||||
},
|
||||
pm.RegisterMultiSelect("Assignees", []string{}, []string{"hubot", "monalisa"}, func(_ string, _, _ []string) ([]int, error) {
|
||||
return []int{0}, nil
|
||||
})
|
||||
pm.RegisterMultiSelect("Labels", []string{}, []string{"help wanted", "good first issue"}, func(_ string, _, _ []string) ([]int, error) {
|
||||
return []int{1}, nil
|
||||
})
|
||||
pm.RegisterMultiSelect("Projects", []string{}, []string{"Huge Refactoring", "The road to 1.0"}, func(_ string, _, _ []string) ([]int, error) {
|
||||
return []int{1}, nil
|
||||
})
|
||||
pm.RegisterSelect("Milestone", []string{"(none)", "1.2 patch release"}, func(_, _ string, _ []string) (int, error) {
|
||||
return 0, nil
|
||||
})
|
||||
|
||||
state := &IssueMetadataState{
|
||||
Assignees: []string{"hubot"},
|
||||
Type: PRMetadata,
|
||||
}
|
||||
err := MetadataSurvey(ios, repo, fetcher, state)
|
||||
err := MetadataSurvey(pm, ios, repo, fetcher, state)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "", stdout.String())
|
||||
|
|
@ -112,33 +99,21 @@ func TestMetadataSurvey_keepExisting(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
//nolint:staticcheck // SA1019: prompt.InitAskStubber is deprecated: use NewAskStubber
|
||||
as, restoreAsk := prompt.InitAskStubber()
|
||||
defer restoreAsk()
|
||||
|
||||
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
|
||||
as.Stub([]*prompt.QuestionStub{
|
||||
{
|
||||
Name: "metadata",
|
||||
Value: []string{"Labels", "Projects"},
|
||||
},
|
||||
pm := prompter.NewMockPrompter(t)
|
||||
pm.RegisterMultiSelect("What would you like to add?", []string{}, []string{"Assignees", "Labels", "Projects", "Milestone"}, func(_ string, _, _ []string) ([]int, error) {
|
||||
return []int{1, 2}, nil
|
||||
})
|
||||
//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
|
||||
as.Stub([]*prompt.QuestionStub{
|
||||
{
|
||||
Name: "labels",
|
||||
Value: []string{"good first issue"},
|
||||
},
|
||||
{
|
||||
Name: "projects",
|
||||
Value: []string{"The road to 1.0"},
|
||||
},
|
||||
pm.RegisterMultiSelect("Labels", []string{}, []string{"help wanted", "good first issue"}, func(_ string, _, _ []string) ([]int, error) {
|
||||
return []int{1}, nil
|
||||
})
|
||||
pm.RegisterMultiSelect("Projects", []string{}, []string{"Huge Refactoring", "The road to 1.0"}, func(_ string, _, _ []string) ([]int, error) {
|
||||
return []int{1}, nil
|
||||
})
|
||||
|
||||
state := &IssueMetadataState{
|
||||
Assignees: []string{"hubot"},
|
||||
}
|
||||
err := MetadataSurvey(ios, repo, fetcher, state)
|
||||
err := MetadataSurvey(pm, ios, repo, fetcher, state)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "", stdout.String())
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package close
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
|
|
@ -56,8 +55,7 @@ func TestNewCmdClose(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
t.Setenv("GH_TOKEN", "auth-token")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package copy
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
|
|
@ -75,8 +74,7 @@ func TestNewCmdCopy(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
t.Setenv("GH_TOKEN", "auth-token")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package create
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
|
|
@ -45,8 +44,7 @@ func TestNewCmdCreate(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
t.Setenv("GH_TOKEN", "auth-token")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package delete
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
|
|
@ -49,8 +48,7 @@ func TestNewCmdDelete(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
t.Setenv("GH_TOKEN", "auth-token")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package edit
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
|
|
@ -92,8 +91,7 @@ func TestNewCmdEdit(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
t.Setenv("GH_TOKEN", "auth-token")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package fieldcreate
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
|
|
@ -79,8 +78,7 @@ func TestNewCmdCreateField(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
t.Setenv("GH_TOKEN", "auth-token")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package fielddelete
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
|
|
@ -43,8 +42,7 @@ func TestNewCmdDeleteField(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
t.Setenv("GH_TOKEN", "auth-token")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package fieldlist
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/internal/tableprinter"
|
||||
|
|
@ -53,8 +52,7 @@ func TestNewCmdList(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
t.Setenv("GH_TOKEN", "auth-token")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package itemadd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
|
|
@ -65,8 +64,7 @@ func TestNewCmdaddItem(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
t.Setenv("GH_TOKEN", "auth-token")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package itemarchive
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
|
|
@ -73,8 +72,7 @@ func TestNewCmdarchiveItem(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
t.Setenv("GH_TOKEN", "auth-token")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package itemcreate
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
|
|
@ -73,8 +72,7 @@ func TestNewCmdCreateItem(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
t.Setenv("GH_TOKEN", "auth-token")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package itemdelete
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
|
|
@ -65,8 +64,7 @@ func TestNewCmdDeleteItem(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
t.Setenv("GH_TOKEN", "auth-token")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package itemedit
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
|
|
@ -105,8 +104,7 @@ func TestNewCmdeditItem(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
t.Setenv("GH_TOKEN", "auth-token")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package itemlist
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
|
|
@ -54,8 +53,7 @@ func TestNewCmdList(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
t.Setenv("GH_TOKEN", "auth-token")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ type listConfig struct {
|
|||
func NewCmdList(f *cmdutil.Factory, runF func(config listConfig) error) *cobra.Command {
|
||||
opts := listOpts{}
|
||||
listCmd := &cobra.Command{
|
||||
Short: "List the projects for an owner",
|
||||
Use: "list",
|
||||
Short: "List the projects for an owner",
|
||||
Example: heredoc.Doc(`
|
||||
# list the current user's projects
|
||||
gh project list
|
||||
|
|
@ -42,6 +42,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(config listConfig) error) *cobra.C
|
|||
# list the projects for org github including closed projects
|
||||
gh project list --owner github --closed
|
||||
`),
|
||||
Aliases: []string{"ls"},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := client.New(f)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package list
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/internal/tableprinter"
|
||||
|
|
@ -56,8 +55,7 @@ func TestNewCmdlist(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
t.Setenv("GH_TOKEN", "auth-token")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import (
|
|||
cmdItemEdit "github.com/cli/cli/v2/pkg/cmd/project/item-edit"
|
||||
cmdItemList "github.com/cli/cli/v2/pkg/cmd/project/item-list"
|
||||
cmdList "github.com/cli/cli/v2/pkg/cmd/project/list"
|
||||
cmdTemplate "github.com/cli/cli/v2/pkg/cmd/project/template"
|
||||
cmdView "github.com/cli/cli/v2/pkg/cmd/project/view"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -24,7 +25,7 @@ import (
|
|||
|
||||
func NewCmdProject(f *cmdutil.Factory) *cobra.Command {
|
||||
var cmd = &cobra.Command{
|
||||
Use: "project <command> [flags]",
|
||||
Use: "project <command>",
|
||||
Short: "Work with GitHub Projects.",
|
||||
Long: "Work with GitHub Projects. Note that the token you are using must have 'project' scope, which is not set by default. You can verify your token scope by running 'gh auth status' and add the project scope by running 'gh auth refresh -s project'.",
|
||||
Example: heredoc.Doc(`
|
||||
|
|
@ -43,6 +44,7 @@ func NewCmdProject(f *cmdutil.Factory) *cobra.Command {
|
|||
cmd.AddCommand(cmdDelete.NewCmdDelete(f, nil))
|
||||
cmd.AddCommand(cmdEdit.NewCmdEdit(f, nil))
|
||||
cmd.AddCommand(cmdView.NewCmdView(f, nil))
|
||||
cmd.AddCommand(cmdTemplate.NewCmdTemplate(f, nil))
|
||||
|
||||
// items
|
||||
cmd.AddCommand(cmdItemList.NewCmdList(f, nil))
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ func JSONProject(project queries.Project) ([]byte, error) {
|
|||
ShortDescription: project.ShortDescription,
|
||||
Public: project.Public,
|
||||
Closed: project.Closed,
|
||||
Template: project.Template,
|
||||
Title: project.Title,
|
||||
ID: project.ID,
|
||||
Readme: project.Readme,
|
||||
|
|
@ -49,6 +50,7 @@ func JSONProjects(projects []queries.Project, totalCount int) ([]byte, error) {
|
|||
ShortDescription: p.ShortDescription,
|
||||
Public: p.Public,
|
||||
Closed: p.Closed,
|
||||
Template: p.Template,
|
||||
Title: p.Title,
|
||||
ID: p.ID,
|
||||
Readme: p.Readme,
|
||||
|
|
@ -87,6 +89,7 @@ type projectJSON struct {
|
|||
ShortDescription string `json:"shortDescription"`
|
||||
Public bool `json:"public"`
|
||||
Closed bool `json:"closed"`
|
||||
Template bool `json:"template"`
|
||||
Title string `json:"title"`
|
||||
ID string `json:"id"`
|
||||
Readme string `json:"readme"`
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ func TestJSONProject_User(t *testing.T) {
|
|||
b, err := JSONProject(project)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"User","login":"monalisa"}}`, string(b))
|
||||
assert.Equal(t, `{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"template":false,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"User","login":"monalisa"}}`, string(b))
|
||||
}
|
||||
|
||||
func TestJSONProject_Org(t *testing.T) {
|
||||
|
|
@ -36,6 +36,7 @@ func TestJSONProject_Org(t *testing.T) {
|
|||
ShortDescription: "short description",
|
||||
Public: true,
|
||||
Readme: "readme",
|
||||
Template: true,
|
||||
}
|
||||
|
||||
project.Items.TotalCount = 1
|
||||
|
|
@ -45,7 +46,7 @@ func TestJSONProject_Org(t *testing.T) {
|
|||
b, err := JSONProject(project)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"Organization","login":"github"}}`, string(b))
|
||||
assert.Equal(t, `{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"template":true,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"Organization","login":"github"}}`, string(b))
|
||||
}
|
||||
|
||||
func TestJSONProjects(t *testing.T) {
|
||||
|
|
@ -70,6 +71,7 @@ func TestJSONProjects(t *testing.T) {
|
|||
ShortDescription: "short description",
|
||||
Public: true,
|
||||
Readme: "readme",
|
||||
Template: true,
|
||||
}
|
||||
|
||||
orgProject.Items.TotalCount = 1
|
||||
|
|
@ -81,7 +83,7 @@ func TestJSONProjects(t *testing.T) {
|
|||
|
||||
assert.Equal(
|
||||
t,
|
||||
`{"projects":[{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"User","login":"monalisa"}},{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"Organization","login":"github"}}],"totalCount":2}`,
|
||||
`{"projects":[{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"template":false,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"User","login":"monalisa"}},{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"template":true,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"Organization","login":"github"}}],"totalCount":2}`,
|
||||
string(b))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ type Project struct {
|
|||
ShortDescription string
|
||||
Public bool
|
||||
Closed bool
|
||||
Template bool
|
||||
Title string
|
||||
ID string
|
||||
Readme string
|
||||
|
|
|
|||
176
pkg/cmd/project/template/template.go
Normal file
176
pkg/cmd/project/template/template.go
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type templateOpts struct {
|
||||
owner string
|
||||
undo bool
|
||||
number int32
|
||||
projectID string
|
||||
format string
|
||||
}
|
||||
|
||||
type templateConfig struct {
|
||||
client *queries.Client
|
||||
opts templateOpts
|
||||
io *iostreams.IOStreams
|
||||
}
|
||||
|
||||
type markProjectTemplateMutation struct {
|
||||
TemplateProject struct {
|
||||
Project queries.Project `graphql:"projectV2"`
|
||||
} `graphql:"markProjectV2AsTemplate(input:$input)"`
|
||||
}
|
||||
type unmarkProjectTemplateMutation struct {
|
||||
TemplateProject struct {
|
||||
Project queries.Project `graphql:"projectV2"`
|
||||
} `graphql:"unmarkProjectV2AsTemplate(input:$input)"`
|
||||
}
|
||||
|
||||
func NewCmdTemplate(f *cmdutil.Factory, runF func(config templateConfig) error) *cobra.Command {
|
||||
opts := templateOpts{}
|
||||
templateCmd := &cobra.Command{
|
||||
Short: "Mark a project as a template",
|
||||
Use: "template [<number>]",
|
||||
Example: heredoc.Doc(`
|
||||
# mark the github org's project "1" as a template
|
||||
gh project template 1 --owner "github"
|
||||
|
||||
# unmark the github org's project "1" as a template
|
||||
gh project template 1 --owner "github" --undo
|
||||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := client.New(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
num, err := strconv.ParseInt(args[0], 10, 32)
|
||||
if err != nil {
|
||||
return cmdutil.FlagErrorf("invalid number: %v", args[0])
|
||||
}
|
||||
opts.number = int32(num)
|
||||
}
|
||||
|
||||
config := templateConfig{
|
||||
client: client,
|
||||
opts: opts,
|
||||
io: f.IOStreams,
|
||||
}
|
||||
|
||||
// allow testing of the command without actually running it
|
||||
if runF != nil {
|
||||
return runF(config)
|
||||
}
|
||||
return runTemplate(config)
|
||||
},
|
||||
}
|
||||
|
||||
templateCmd.Flags().StringVar(&opts.owner, "owner", "", "Login of the org owner.")
|
||||
templateCmd.Flags().BoolVar(&opts.undo, "undo", false, "Unmark the project as a template.")
|
||||
cmdutil.StringEnumFlag(templateCmd, &opts.format, "format", "", "", []string{"json"}, "Output format")
|
||||
|
||||
return templateCmd
|
||||
}
|
||||
|
||||
func runTemplate(config templateConfig) error {
|
||||
canPrompt := config.io.CanPrompt()
|
||||
owner, err := config.client.NewOwner(canPrompt, config.opts.owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := config.client.NewProject(canPrompt, owner, config.opts.number, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.opts.projectID = project.ID
|
||||
|
||||
if config.opts.undo {
|
||||
query, variables := unmarkTemplateArgs(config)
|
||||
err = config.client.Mutate("UnmarkProjectTemplate", query, variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.opts.format == "json" {
|
||||
return printJSON(config, *project)
|
||||
}
|
||||
|
||||
return printResults(config, query.TemplateProject.Project)
|
||||
|
||||
}
|
||||
query, variables := markTemplateArgs(config)
|
||||
err = config.client.Mutate("MarkProjectTemplate", query, variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.opts.format == "json" {
|
||||
return printJSON(config, *project)
|
||||
}
|
||||
|
||||
return printResults(config, query.TemplateProject.Project)
|
||||
}
|
||||
|
||||
func markTemplateArgs(config templateConfig) (*markProjectTemplateMutation, map[string]interface{}) {
|
||||
return &markProjectTemplateMutation{}, map[string]interface{}{
|
||||
"input": githubv4.MarkProjectV2AsTemplateInput{
|
||||
ProjectID: githubv4.ID(config.opts.projectID),
|
||||
},
|
||||
"firstItems": githubv4.Int(0),
|
||||
"afterItems": (*githubv4.String)(nil),
|
||||
"firstFields": githubv4.Int(0),
|
||||
"afterFields": (*githubv4.String)(nil),
|
||||
}
|
||||
}
|
||||
|
||||
func unmarkTemplateArgs(config templateConfig) (*unmarkProjectTemplateMutation, map[string]interface{}) {
|
||||
return &unmarkProjectTemplateMutation{}, map[string]interface{}{
|
||||
"input": githubv4.UnmarkProjectV2AsTemplateInput{
|
||||
ProjectID: githubv4.ID(config.opts.projectID),
|
||||
},
|
||||
"firstItems": githubv4.Int(0),
|
||||
"afterItems": (*githubv4.String)(nil),
|
||||
"firstFields": githubv4.Int(0),
|
||||
"afterFields": (*githubv4.String)(nil),
|
||||
}
|
||||
}
|
||||
|
||||
func printResults(config templateConfig, project queries.Project) error {
|
||||
if !config.io.IsStdoutTTY() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if config.opts.undo {
|
||||
_, err := fmt.Fprintf(config.io.Out, "Unmarked project %d as a template.\n", project.Number)
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := fmt.Fprintf(config.io.Out, "Marked project %d as a template.\n", project.Number)
|
||||
return err
|
||||
}
|
||||
|
||||
func printJSON(config templateConfig, project queries.Project) error {
|
||||
b, err := format.JSONProject(project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = config.io.Out.Write(b)
|
||||
return err
|
||||
}
|
||||
275
pkg/cmd/project/template/template_test.go
Normal file
275
pkg/cmd/project/template/template_test.go
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
func TestNewCmdTemplate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cli string
|
||||
wants templateOpts
|
||||
wantsErr bool
|
||||
wantsErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "not-a-number",
|
||||
cli: "x",
|
||||
wantsErr: true,
|
||||
wantsErrMsg: "invalid number: x",
|
||||
},
|
||||
{
|
||||
name: "number",
|
||||
cli: "123",
|
||||
wants: templateOpts{
|
||||
number: 123,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "owner",
|
||||
cli: "--owner monalisa",
|
||||
wants: templateOpts{
|
||||
owner: "monalisa",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "undo",
|
||||
cli: "--undo",
|
||||
wants: templateOpts{
|
||||
undo: true,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "json",
|
||||
cli: "--format json",
|
||||
wants: templateOpts{
|
||||
format: "json",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Setenv("GH_TOKEN", "auth-token")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.cli)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts templateOpts
|
||||
cmd := NewCmdTemplate(f, func(config templateConfig) error {
|
||||
gotOpts = config.opts
|
||||
return nil
|
||||
})
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tt.wantsErrMsg, err.Error())
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.wants.number, gotOpts.number)
|
||||
assert.Equal(t, tt.wants.owner, gotOpts.owner)
|
||||
assert.Equal(t, tt.wants.format, gotOpts.format)
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestRunMarkTemplate_Org(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get org ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"user"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query OrgProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// template project
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation MarkProjectTemplate.*","variables":{"afterFields":null,"afterItems":null,"firstFields":0,"firstItems":0,"input":{"projectId":"an ID"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"markProjectV2AsTemplate": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "project ID",
|
||||
"number": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := templateConfig{
|
||||
opts: templateOpts{
|
||||
owner: "github",
|
||||
number: 1,
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runTemplate(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Marked project 1 as a template.\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunUnmarkTemplate_Org(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get org ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"user"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query OrgProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// template project
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation UnmarkProjectTemplate.*","variables":{"afterFields":null,"afterItems":null,"firstFields":0,"firstItems":0,"input":{"projectId":"an ID"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"unmarkProjectV2AsTemplate": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "project ID",
|
||||
"number": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := templateConfig{
|
||||
opts: templateOpts{
|
||||
owner: "github",
|
||||
number: 1,
|
||||
undo: true,
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runTemplate(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Unmarked project 1 as a template.\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ package view
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
|
|
@ -57,8 +56,7 @@ func TestNewCmdview(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
t.Setenv("GH_TOKEN", "auth-token")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue