Merge remote-tracking branch 'origin' into pr-lookup-refactor
This commit is contained in:
commit
bc3bb97c43
52 changed files with 1128 additions and 385 deletions
|
|
@ -48,26 +48,26 @@ func actionsExplainer(cs *iostreams.ColorScheme) string {
|
|||
|
||||
GitHub CLI integrates with Actions to help you manage runs and workflows.
|
||||
|
||||
%s
|
||||
gh run list: List recent workflow runs
|
||||
gh run view: View details for a workflow run or one of its jobs
|
||||
gh run watch: Watch a workflow run while it executes
|
||||
gh run rerun: Rerun a failed workflow run
|
||||
%s
|
||||
gh run list: List recent workflow runs
|
||||
gh run view: View details for a workflow run or one of its jobs
|
||||
gh run watch: Watch a workflow run while it executes
|
||||
gh run rerun: Rerun a failed workflow run
|
||||
gh run download: Download artifacts generated by runs
|
||||
|
||||
To see more help, run 'gh help run <subcommand>'
|
||||
|
||||
%s
|
||||
gh workflow list: List all the workflow files in your repository
|
||||
gh workflow view: View details for a workflow file
|
||||
gh workflow enable: Enable a workflow file
|
||||
gh workflow disable: Disable a workflow file
|
||||
%s
|
||||
gh workflow list: List all the workflow files in your repository
|
||||
gh workflow view: View details for a workflow file
|
||||
gh workflow enable: Enable a workflow file
|
||||
gh workflow disable: Disable a workflow file
|
||||
gh workflow run: Trigger a workflow_dispatch run for a workflow file
|
||||
|
||||
To see more help, run 'gh help workflow <subcommand>'
|
||||
|
||||
For more in depth help including examples, see online documentation at:
|
||||
https://docs.github.com/en/actions/guides/managing-github-actions-with-github-cli
|
||||
<https://docs.github.com/en/actions/guides/managing-github-actions-with-github-cli>
|
||||
`, header, runHeader, workflowHeader)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -71,8 +71,10 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
|
|||
The endpoint argument should either be a path of a GitHub API v3 endpoint, or
|
||||
"graphql" to access the GitHub API v4.
|
||||
|
||||
Placeholder values ":owner", ":repo", and ":branch" in the endpoint argument will
|
||||
get replaced with values from the repository of the current directory.
|
||||
Placeholder values "{owner}", "{repo}", and "{branch}" in the endpoint argument will
|
||||
get replaced with values from the repository of the current directory. Note that in
|
||||
some shells, for example PowerShell, you may need to enclose any value that contains
|
||||
"{...}" in quotes to prevent the shell from applying special meaning to curly braces.
|
||||
|
||||
The default HTTP request method is "GET" normally and "POST" if any parameters
|
||||
were added. Override the method with %[1]s--method%[1]s.
|
||||
|
|
@ -87,7 +89,7 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
|
|||
|
||||
- literal values "true", "false", "null", and integer numbers get converted to
|
||||
appropriate JSON types;
|
||||
- placeholder values ":owner", ":repo", and ":branch" get populated with values
|
||||
- placeholder values "{owner}", "{repo}", and "{branch}" get populated with values
|
||||
from the repository of the current directory;
|
||||
- if the value starts with "@", the rest of the value is interpreted as a
|
||||
filename to read the value from. Pass "-" to read from standard input.
|
||||
|
|
@ -106,10 +108,10 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
|
|||
`, "`"),
|
||||
Example: heredoc.Doc(`
|
||||
# list releases in the current repository
|
||||
$ gh api repos/:owner/:repo/releases
|
||||
$ gh api repos/{owner}/{repo}/releases
|
||||
|
||||
# post an issue comment
|
||||
$ gh api repos/:owner/:repo/issues/123/comments -f body='Hi from CLI'
|
||||
$ gh api repos/{owner}/{repo}/issues/123/comments -f body='Hi from CLI'
|
||||
|
||||
# add parameters to a GET request
|
||||
$ gh api -X GET search/issues -f q='repo:cli/cli is:open remote'
|
||||
|
|
@ -121,14 +123,14 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
|
|||
$ gh api --preview baptiste,nebula ...
|
||||
|
||||
# print only specific fields from the response
|
||||
$ gh api repos/:owner/:repo/issues --jq '.[].title'
|
||||
$ gh api repos/{owner}/{repo}/issues --jq '.[].title'
|
||||
|
||||
# use a template for the output
|
||||
$ gh api repos/:owner/:repo/issues --template \
|
||||
$ gh api repos/{owner}/{repo}/issues --template \
|
||||
'{{range .}}{{.title}} ({{.labels | pluck "name" | join ", " | color "yellow"}}){{"\n"}}{{end}}'
|
||||
|
||||
# list releases with GraphQL
|
||||
$ gh api graphql -F owner=':owner' -F name=':repo' -f query='
|
||||
$ gh api graphql -F owner='{owner}' -F name='{repo}' -f query='
|
||||
query($name: String!, $owner: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
releases(last: 3) {
|
||||
|
|
@ -397,41 +399,41 @@ func processResponse(resp *http.Response, opts *ApiOptions, headersOutputStream
|
|||
return
|
||||
}
|
||||
|
||||
var placeholderRE = regexp.MustCompile(`\:(owner|repo|branch)\b`)
|
||||
var placeholderRE = regexp.MustCompile(`(\:(owner|repo|branch)\b|\{[a-z]+\})`)
|
||||
|
||||
// fillPlaceholders populates `:owner` and `:repo` placeholders with values from the current repository
|
||||
// fillPlaceholders replaces placeholders with values from the current repository
|
||||
func fillPlaceholders(value string, opts *ApiOptions) (string, error) {
|
||||
if !placeholderRE.MatchString(value) {
|
||||
return value, nil
|
||||
}
|
||||
var err error
|
||||
return placeholderRE.ReplaceAllStringFunc(value, func(m string) string {
|
||||
var name string
|
||||
if m[0] == ':' {
|
||||
name = m[1:]
|
||||
} else {
|
||||
name = m[1 : len(m)-1]
|
||||
}
|
||||
|
||||
baseRepo, err := opts.BaseRepo()
|
||||
if err != nil {
|
||||
return value, err
|
||||
}
|
||||
|
||||
filled := placeholderRE.ReplaceAllStringFunc(value, func(m string) string {
|
||||
switch m {
|
||||
case ":owner":
|
||||
return baseRepo.RepoOwner()
|
||||
case ":repo":
|
||||
return baseRepo.RepoName()
|
||||
case ":branch":
|
||||
branch, e := opts.Branch()
|
||||
if e != nil {
|
||||
switch name {
|
||||
case "owner":
|
||||
if baseRepo, e := opts.BaseRepo(); e == nil {
|
||||
return baseRepo.RepoOwner()
|
||||
} else {
|
||||
err = e
|
||||
}
|
||||
case "repo":
|
||||
if baseRepo, e := opts.BaseRepo(); e == nil {
|
||||
return baseRepo.RepoName()
|
||||
} else {
|
||||
err = e
|
||||
}
|
||||
case "branch":
|
||||
if branch, e := opts.Branch(); e == nil {
|
||||
return branch
|
||||
} else {
|
||||
err = e
|
||||
}
|
||||
return branch
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid placeholder: %q", m))
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return value, err
|
||||
}
|
||||
|
||||
return filled, nil
|
||||
return m
|
||||
}), err
|
||||
}
|
||||
|
||||
func printHeaders(w io.Writer, headers http.Header, colorize bool) {
|
||||
|
|
|
|||
|
|
@ -693,6 +693,9 @@ func Test_apiRun_inputFile(t *testing.T) {
|
|||
contentLength: 10,
|
||||
},
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
io, stdin, _, _ := iostreams.Test()
|
||||
|
|
@ -702,13 +705,12 @@ func Test_apiRun_inputFile(t *testing.T) {
|
|||
if tt.inputFile == "-" {
|
||||
_, _ = stdin.Write(tt.inputContents)
|
||||
} else {
|
||||
f, err := ioutil.TempFile("", tt.inputFile)
|
||||
f, err := ioutil.TempFile(tempDir, tt.inputFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _ = f.Write(tt.inputContents)
|
||||
f.Close()
|
||||
t.Cleanup(func() { os.Remove(f.Name()) })
|
||||
defer f.Close()
|
||||
inputFile = f.Name()
|
||||
}
|
||||
|
||||
|
|
@ -825,13 +827,13 @@ func Test_parseFields(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_magicFieldValue(t *testing.T) {
|
||||
f, err := ioutil.TempFile("", "gh-test")
|
||||
f, err := ioutil.TempFile(t.TempDir(), "gh-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fmt.Fprint(f, "file contents")
|
||||
f.Close()
|
||||
t.Cleanup(func() { os.Remove(f.Name()) })
|
||||
|
||||
io, _, _, _ := iostreams.Test()
|
||||
|
||||
|
|
@ -870,7 +872,7 @@ func Test_magicFieldValue(t *testing.T) {
|
|||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "placeholder",
|
||||
name: "placeholder colon",
|
||||
args: args{
|
||||
v: ":owner",
|
||||
opts: &ApiOptions{
|
||||
|
|
@ -883,6 +885,20 @@ func Test_magicFieldValue(t *testing.T) {
|
|||
want: "hubot",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "placeholder braces",
|
||||
args: args{
|
||||
v: "{owner}",
|
||||
opts: &ApiOptions{
|
||||
IO: io,
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("hubot", "robot-uprising"), nil
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "hubot",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "file",
|
||||
args: args{
|
||||
|
|
@ -918,13 +934,13 @@ func Test_magicFieldValue(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_openUserFile(t *testing.T) {
|
||||
f, err := ioutil.TempFile("", "gh-test")
|
||||
f, err := ioutil.TempFile(t.TempDir(), "gh-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fmt.Fprint(f, "file contents")
|
||||
f.Close()
|
||||
t.Cleanup(func() { os.Remove(f.Name()) })
|
||||
|
||||
file, length, err := openUserFile(f.Name(), nil)
|
||||
if err != nil {
|
||||
|
|
@ -964,7 +980,7 @@ func Test_fillPlaceholders(t *testing.T) {
|
|||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "has substitutes",
|
||||
name: "has substitutes (colon)",
|
||||
args: args{
|
||||
value: "repos/:owner/:repo/releases",
|
||||
opts: &ApiOptions{
|
||||
|
|
@ -977,39 +993,96 @@ func Test_fillPlaceholders(t *testing.T) {
|
|||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "has branch placeholder",
|
||||
name: "has branch placeholder (colon)",
|
||||
args: args{
|
||||
value: "repos/cli/cli/branches/:branch/protection/required_status_checks",
|
||||
value: "repos/owner/repo/branches/:branch/protection/required_status_checks",
|
||||
opts: &ApiOptions{
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("cli", "cli"), nil
|
||||
},
|
||||
BaseRepo: nil,
|
||||
Branch: func() (string, error) {
|
||||
return "trunk", nil
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "repos/cli/cli/branches/trunk/protection/required_status_checks",
|
||||
want: "repos/owner/repo/branches/trunk/protection/required_status_checks",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "has branch placeholder and git is in detached head",
|
||||
name: "has branch placeholder and git is in detached head (colon)",
|
||||
args: args{
|
||||
value: "repos/:owner/:repo/branches/:branch",
|
||||
opts: &ApiOptions{
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("cli", "cli"), nil
|
||||
return ghrepo.New("hubot", "robot-uprising"), nil
|
||||
},
|
||||
Branch: func() (string, error) {
|
||||
return "", git.ErrNotOnAnyBranch
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "repos/:owner/:repo/branches/:branch",
|
||||
want: "repos/hubot/robot-uprising/branches/:branch",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no greedy substitutes",
|
||||
name: "has substitutes",
|
||||
args: args{
|
||||
value: "repos/{owner}/{repo}/releases",
|
||||
opts: &ApiOptions{
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("hubot", "robot-uprising"), nil
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "repos/hubot/robot-uprising/releases",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "has branch placeholder",
|
||||
args: args{
|
||||
value: "repos/owner/repo/branches/{branch}/protection/required_status_checks",
|
||||
opts: &ApiOptions{
|
||||
BaseRepo: nil,
|
||||
Branch: func() (string, error) {
|
||||
return "trunk", nil
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "repos/owner/repo/branches/trunk/protection/required_status_checks",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "has branch placeholder and git is in detached head",
|
||||
args: args{
|
||||
value: "repos/{owner}/{repo}/branches/{branch}",
|
||||
opts: &ApiOptions{
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("hubot", "robot-uprising"), nil
|
||||
},
|
||||
Branch: func() (string, error) {
|
||||
return "", git.ErrNotOnAnyBranch
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "repos/hubot/robot-uprising/branches/{branch}",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "surfaces errors in earlier placeholders",
|
||||
args: args{
|
||||
value: "{branch}-{owner}",
|
||||
opts: &ApiOptions{
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("hubot", "robot-uprising"), nil
|
||||
},
|
||||
Branch: func() (string, error) {
|
||||
return "", git.ErrNotOnAnyBranch
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "{branch}-hubot",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no greedy substitutes (colon)",
|
||||
args: args{
|
||||
value: ":ownership/:repository",
|
||||
opts: &ApiOptions{
|
||||
|
|
@ -1019,6 +1092,17 @@ func Test_fillPlaceholders(t *testing.T) {
|
|||
want: ":ownership/:repository",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "non-placeholders are left intact",
|
||||
args: args{
|
||||
value: "{}{ownership}/{repository}",
|
||||
opts: &ApiOptions{
|
||||
BaseRepo: nil,
|
||||
},
|
||||
},
|
||||
want: "{}{ownership}/{repository}",
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ func NewCmdCompletion(io *iostreams.IOStreams) *cobra.Command {
|
|||
|
||||
When installing GitHub CLI through a package manager, it's possible that
|
||||
no additional shell configuration is necessary to gain completion support. For
|
||||
Homebrew, see https://docs.brew.sh/Shell-Completion
|
||||
Homebrew, see <https://docs.brew.sh/Shell-Completion>
|
||||
|
||||
If you need to set up completions manually, follow the instructions below. The exact
|
||||
config file locations might vary based on your system. Make sure to restart your
|
||||
|
|
|
|||
|
|
@ -151,7 +151,13 @@ func createRun(opts *CreateOptions) error {
|
|||
var httpError api.HTTPError
|
||||
if errors.As(err, &httpError) {
|
||||
if httpError.OAuthScopes != "" && !strings.Contains(httpError.OAuthScopes, "gist") {
|
||||
return fmt.Errorf("This command requires the 'gist' OAuth scope.\nPlease re-authenticate by doing `gh config set -h github.com oauth_token ''` and running the command again.")
|
||||
return fmt.Errorf("This command requires the 'gist' OAuth scope.\nPlease re-authenticate with: gh auth refresh -h %s -s gist", host)
|
||||
}
|
||||
if httpError.StatusCode == http.StatusUnprocessableEntity {
|
||||
if detectEmptyFiles(files) {
|
||||
fmt.Fprintf(errOut, "%s Failed to create gist: %s\n", cs.FailureIcon(), "a gist file cannot be blank")
|
||||
return cmdutil.SilentError
|
||||
}
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%s Failed to create gist: %w", cs.Red("X"), err)
|
||||
|
|
@ -266,3 +272,12 @@ func createGist(client *http.Client, hostname, description string, public bool,
|
|||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func detectEmptyFiles(files map[string]*shared.GistFile) bool {
|
||||
for _, file := range files {
|
||||
if strings.TrimSpace(file.Content) == "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import (
|
|||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/run"
|
||||
"github.com/cli/cli/pkg/cmd/gist/shared"
|
||||
|
|
@ -18,10 +20,6 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
fixtureFile = "../fixture.txt"
|
||||
)
|
||||
|
||||
func Test_processFiles(t *testing.T) {
|
||||
fakeStdin := strings.NewReader("hey cool how is it going")
|
||||
files, err := processFiles(ioutil.NopCloser(fakeStdin), "", []string{"-"})
|
||||
|
|
@ -164,15 +162,22 @@ func TestNewCmdCreate(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_createRun(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
fixtureFile := path.Join(tempDir, "fixture.txt")
|
||||
assert.NoError(t, ioutil.WriteFile(fixtureFile, []byte("{}"), 0644))
|
||||
emptyFile := path.Join(tempDir, "empty.txt")
|
||||
assert.NoError(t, ioutil.WriteFile(emptyFile, []byte(" \t\n"), 0644))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
opts *CreateOptions
|
||||
stdin string
|
||||
wantOut string
|
||||
wantStderr string
|
||||
wantParams map[string]interface{}
|
||||
wantErr bool
|
||||
wantBrowse string
|
||||
name string
|
||||
opts *CreateOptions
|
||||
stdin string
|
||||
wantOut string
|
||||
wantStderr string
|
||||
wantParams map[string]interface{}
|
||||
wantErr bool
|
||||
wantBrowse string
|
||||
responseStatus int
|
||||
}{
|
||||
{
|
||||
name: "public",
|
||||
|
|
@ -193,6 +198,7 @@ func Test_createRun(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
responseStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "with description",
|
||||
|
|
@ -213,6 +219,7 @@ func Test_createRun(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
responseStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "multiple files",
|
||||
|
|
@ -236,6 +243,28 @@ func Test_createRun(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
responseStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "file with empty content",
|
||||
opts: &CreateOptions{
|
||||
Filenames: []string{emptyFile},
|
||||
},
|
||||
wantOut: "",
|
||||
wantStderr: heredoc.Doc(`
|
||||
- Creating gist empty.txt
|
||||
X Failed to create gist: a gist file cannot be blank
|
||||
`),
|
||||
wantErr: true,
|
||||
wantParams: map[string]interface{}{
|
||||
"description": "",
|
||||
"updated_at": "0001-01-01T00:00:00Z",
|
||||
"public": false,
|
||||
"files": map[string]interface{}{
|
||||
"empty.txt": map[string]interface{}{"content": " \t\n"},
|
||||
},
|
||||
},
|
||||
responseStatus: http.StatusUnprocessableEntity,
|
||||
},
|
||||
{
|
||||
name: "stdin arg",
|
||||
|
|
@ -256,6 +285,7 @@ func Test_createRun(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
responseStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "web arg",
|
||||
|
|
@ -277,14 +307,22 @@ func Test_createRun(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
responseStatus: http.StatusOK,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
reg := &httpmock.Registry{}
|
||||
reg.Register(httpmock.REST("POST", "gists"),
|
||||
httpmock.JSONResponse(struct {
|
||||
Html_url string
|
||||
}{"https://gist.github.com/aa5a315d61ae9438b18d"}))
|
||||
if tt.responseStatus == http.StatusOK {
|
||||
reg.Register(
|
||||
httpmock.REST("POST", "gists"),
|
||||
httpmock.StringResponse(`{
|
||||
"html_url": "https://gist.github.com/aa5a315d61ae9438b18d"
|
||||
}`))
|
||||
} else {
|
||||
reg.Register(
|
||||
httpmock.REST("POST", "gists"),
|
||||
httpmock.StatusStringResponse(tt.responseStatus, "{}"))
|
||||
}
|
||||
|
||||
mockClient := func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
|
|
@ -325,6 +363,32 @@ func Test_createRun(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_detectEmptyFiles(t *testing.T) {
|
||||
tests := []struct {
|
||||
content string
|
||||
isEmptyFile bool
|
||||
}{
|
||||
{
|
||||
content: "{}",
|
||||
isEmptyFile: false,
|
||||
},
|
||||
{
|
||||
content: "\n\t",
|
||||
isEmptyFile: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
files := map[string]*shared.GistFile{}
|
||||
files["file"] = &shared.GistFile{
|
||||
Content: tt.content,
|
||||
}
|
||||
|
||||
isEmptyFile := detectEmptyFiles(files)
|
||||
assert.Equal(t, tt.isEmptyFile, isEmptyFile)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CreateRun_reauth(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
reg.Register(httpmock.REST("POST", "gists"), func(req *http.Request) (*http.Response, error) {
|
||||
|
|
@ -332,33 +396,24 @@ func Test_CreateRun_reauth(t *testing.T) {
|
|||
StatusCode: 404,
|
||||
Request: req,
|
||||
Header: map[string][]string{
|
||||
"X-Oauth-Scopes": {"coolScope"},
|
||||
"X-Oauth-Scopes": {"repo, read:org"},
|
||||
},
|
||||
Body: ioutil.NopCloser(bytes.NewBufferString("oh no")),
|
||||
}, nil
|
||||
})
|
||||
|
||||
mockClient := func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
|
||||
io, _, _, _ := iostreams.Test()
|
||||
|
||||
opts := &CreateOptions{
|
||||
IO: io,
|
||||
HttpClient: mockClient,
|
||||
IO: io,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
},
|
||||
Config: func() (config.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
Filenames: []string{fixtureFile},
|
||||
}
|
||||
|
||||
err := createRun(opts)
|
||||
if err == nil {
|
||||
t.Fatalf("expected oauth error")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "Please re-authenticate") {
|
||||
t.Errorf("got unexpected error: %s", err)
|
||||
}
|
||||
assert.EqualError(t, err, "This command requires the 'gist' OAuth scope.\nPlease re-authenticate with: gh auth refresh -h github.com -s gist")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
{}
|
||||
|
|
@ -20,7 +20,7 @@ type GistFile struct {
|
|||
Filename string `json:"filename,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Language string `json:"language,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type GistOwner struct {
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ func closeRun(opts *CloseOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if issue.Closed {
|
||||
if issue.State == "CLOSED" {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Issue #%d (%s) is already closed\n", cs.Yellow("!"), issue.Number, issue.Title)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ func TestIssueClose_alreadyClosed(t *testing.T) {
|
|||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"hasIssuesEnabled": true,
|
||||
"issue": { "number": 13, "title": "The title of the issue", "closed": true}
|
||||
"issue": { "number": 13, "title": "The title of the issue", "state": "CLOSED"}
|
||||
} } }`),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
|
@ -422,8 +421,9 @@ func TestIssueCreate_recover(t *testing.T) {
|
|||
},
|
||||
})
|
||||
|
||||
tmpfile, err := ioutil.TempFile(os.TempDir(), "testrecover*")
|
||||
tmpfile, err := ioutil.TempFile(t.TempDir(), "testrecover*")
|
||||
assert.NoError(t, err)
|
||||
defer tmpfile.Close()
|
||||
|
||||
state := prShared.IssueMetadataState{
|
||||
Title: "recovered title",
|
||||
|
|
|
|||
|
|
@ -155,8 +155,7 @@ func listRun(opts *ListOptions) error {
|
|||
defer opts.IO.StopPager()
|
||||
|
||||
if opts.Exporter != nil {
|
||||
data := api.ExportIssues(listResult.Issues, opts.Exporter.Fields())
|
||||
return opts.Exporter.Write(opts.IO.Out, data, opts.IO.ColorEnabled())
|
||||
return opts.Exporter.Write(opts.IO.Out, listResult.Issues, opts.IO.ColorEnabled())
|
||||
}
|
||||
|
||||
if isTerminal {
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ func reopenRun(opts *ReopenOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if !issue.Closed {
|
||||
if issue.State == "OPEN" {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Issue #%d (%s) is already open\n", cs.Yellow("!"), issue.Number, issue.Title)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ func TestIssueReopen(t *testing.T) {
|
|||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"hasIssuesEnabled": true,
|
||||
"issue": { "id": "THE-ID", "number": 2, "closed": true, "title": "The title of the issue"}
|
||||
"issue": { "id": "THE-ID", "number": 2, "state": "CLOSED", "title": "The title of the issue"}
|
||||
} } }`),
|
||||
)
|
||||
http.Register(
|
||||
|
|
@ -96,7 +96,7 @@ func TestIssueReopen_alreadyOpen(t *testing.T) {
|
|||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"hasIssuesEnabled": true,
|
||||
"issue": { "number": 2, "closed": false, "title": "The title of the issue"}
|
||||
"issue": { "number": 2, "state": "OPEN", "title": "The title of the issue"}
|
||||
} } }`),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -96,11 +96,11 @@ func statusRun(opts *StatusOptions) error {
|
|||
|
||||
if opts.Exporter != nil {
|
||||
data := map[string]interface{}{
|
||||
"createdBy": api.ExportIssues(issuePayload.Authored.Issues, opts.Exporter.Fields()),
|
||||
"assigned": api.ExportIssues(issuePayload.Assigned.Issues, opts.Exporter.Fields()),
|
||||
"mentioned": api.ExportIssues(issuePayload.Mentioned.Issues, opts.Exporter.Fields()),
|
||||
"createdBy": issuePayload.Authored.Issues,
|
||||
"assigned": issuePayload.Assigned.Issues,
|
||||
"mentioned": issuePayload.Mentioned.Issues,
|
||||
}
|
||||
return opts.Exporter.Write(opts.IO.Out, &data, opts.IO.ColorEnabled())
|
||||
return opts.Exporter.Write(opts.IO.Out, data, opts.IO.ColorEnabled())
|
||||
}
|
||||
|
||||
out := opts.IO.Out
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ func Test_transferRunSuccessfulIssueTransfer(t *testing.T) {
|
|||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"hasIssuesEnabled": true,
|
||||
"issue": { "id": "THE-ID", "number": 1234, "closed": true, "title": "The title of the issue"}
|
||||
"issue": { "id": "THE-ID", "number": 1234, "title": "The title of the issue"}
|
||||
} } }`))
|
||||
|
||||
http.Register(
|
||||
|
|
|
|||
|
|
@ -116,8 +116,7 @@ func viewRun(opts *ViewOptions) error {
|
|||
defer opts.IO.StopPager()
|
||||
|
||||
if opts.Exporter != nil {
|
||||
exportIssue := issue.ExportData(opts.Exporter.Fields())
|
||||
return opts.Exporter.Write(opts.IO.Out, exportIssue, opts.IO.ColorEnabled())
|
||||
return opts.Exporter.Write(opts.IO.Out, issue, opts.IO.ColorEnabled())
|
||||
}
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
|
|
|
|||
|
|
@ -681,7 +681,7 @@ func handlePush(opts CreateOptions, ctx CreateContext) error {
|
|||
// one by forking the base repository
|
||||
if headRepo == nil && ctx.IsPushEnabled {
|
||||
opts.IO.StartProgressIndicator()
|
||||
headRepo, err = api.ForkRepo(client, ctx.BaseRepo)
|
||||
headRepo, err = api.ForkRepo(client, ctx.BaseRepo, "")
|
||||
opts.IO.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error forking repo: %w", err)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
|
|
@ -305,8 +304,9 @@ func TestPRCreate_recover(t *testing.T) {
|
|||
},
|
||||
})
|
||||
|
||||
tmpfile, err := ioutil.TempFile(os.TempDir(), "testrecover*")
|
||||
tmpfile, err := ioutil.TempFile(t.TempDir(), "testrecover*")
|
||||
assert.NoError(t, err)
|
||||
defer tmpfile.Close()
|
||||
|
||||
state := prShared.IssueMetadataState{
|
||||
Title: "recovered title",
|
||||
|
|
|
|||
|
|
@ -155,8 +155,7 @@ func listRun(opts *ListOptions) error {
|
|||
defer opts.IO.StopPager()
|
||||
|
||||
if opts.Exporter != nil {
|
||||
data := api.ExportPRs(listResult.PullRequests, opts.Exporter.Fields())
|
||||
return opts.Exporter.Write(opts.IO.Out, data, opts.IO.ColorEnabled())
|
||||
return opts.Exporter.Write(opts.IO.Out, listResult.PullRequests, opts.IO.ColorEnabled())
|
||||
}
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
|
|
@ -70,6 +69,8 @@ func Test_PreserveInput(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.state == nil {
|
||||
|
|
@ -78,9 +79,9 @@ func Test_PreserveInput(t *testing.T) {
|
|||
|
||||
io, _, _, errOut := iostreams.Test()
|
||||
|
||||
tf, tferr := tmpfile()
|
||||
tf, tferr := ioutil.TempFile(tempDir, "testfile*")
|
||||
assert.NoError(t, tferr)
|
||||
defer os.Remove(tf.Name())
|
||||
defer tf.Close()
|
||||
|
||||
io.TempFileOverride = tf
|
||||
|
||||
|
|
@ -111,13 +112,3 @@ func Test_PreserveInput(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func tmpfile() (*os.File, error) {
|
||||
dir := os.TempDir()
|
||||
tmpfile, err := ioutil.TempFile(dir, "testfile*")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tmpfile, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,13 +113,13 @@ func statusRun(opts *StatusOptions) error {
|
|||
if opts.Exporter != nil {
|
||||
data := map[string]interface{}{
|
||||
"currentBranch": nil,
|
||||
"createdBy": api.ExportPRs(prPayload.ViewerCreated.PullRequests, opts.Exporter.Fields()),
|
||||
"needsReview": api.ExportPRs(prPayload.ReviewRequested.PullRequests, opts.Exporter.Fields()),
|
||||
"createdBy": prPayload.ViewerCreated.PullRequests,
|
||||
"needsReview": prPayload.ReviewRequested.PullRequests,
|
||||
}
|
||||
if prPayload.CurrentPR != nil {
|
||||
data["currentBranch"] = prPayload.CurrentPR.ExportData(opts.Exporter.Fields())
|
||||
data["currentBranch"] = prPayload.CurrentPR
|
||||
}
|
||||
return opts.Exporter.Write(opts.IO.Out, &data, opts.IO.ColorEnabled())
|
||||
return opts.Exporter.Write(opts.IO.Out, data, opts.IO.ColorEnabled())
|
||||
}
|
||||
|
||||
out := opts.IO.Out
|
||||
|
|
|
|||
|
|
@ -119,8 +119,7 @@ func viewRun(opts *ViewOptions) error {
|
|||
defer opts.IO.StopPager()
|
||||
|
||||
if opts.Exporter != nil {
|
||||
exportPR := pr.ExportData(opts.Exporter.Fields())
|
||||
return opts.Exporter.Write(opts.IO.Out, exportPR, opts.IO.ColorEnabled())
|
||||
return opts.Exporter.Write(opts.IO.Out, pr, opts.IO.ColorEnabled())
|
||||
}
|
||||
|
||||
if connectedToTerminal {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ type ForkOptions struct {
|
|||
PromptClone bool
|
||||
PromptRemote bool
|
||||
RemoteName string
|
||||
Organization string
|
||||
Rename bool
|
||||
}
|
||||
|
||||
|
|
@ -110,6 +111,7 @@ Additional 'git clone' flags can be passed in by listing them after '--'.`,
|
|||
cmd.Flags().BoolVar(&opts.Clone, "clone", false, "Clone the fork {true|false}")
|
||||
cmd.Flags().BoolVar(&opts.Remote, "remote", false, "Add remote for fork {true|false}")
|
||||
cmd.Flags().StringVar(&opts.RemoteName, "remote-name", "origin", "Specify a name for a fork's new remote.")
|
||||
cmd.Flags().StringVar(&opts.Organization, "org", "", "Create the fork in an organization")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -169,7 +171,7 @@ func forkRun(opts *ForkOptions) error {
|
|||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
opts.IO.StartProgressIndicator()
|
||||
forkedRepo, err := api.ForkRepo(apiClient, repoToFork)
|
||||
forkedRepo, err := api.ForkRepo(apiClient, repoToFork, opts.Organization)
|
||||
opts.IO.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fork: %w", err)
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@ package fork
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/config"
|
||||
|
|
@ -72,8 +74,9 @@ func TestNewCmdFork(t *testing.T) {
|
|||
name: "blank nontty",
|
||||
cli: "",
|
||||
wants: ForkOptions{
|
||||
RemoteName: "origin",
|
||||
Rename: true,
|
||||
RemoteName: "origin",
|
||||
Rename: true,
|
||||
Organization: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -85,6 +88,7 @@ func TestNewCmdFork(t *testing.T) {
|
|||
PromptClone: true,
|
||||
PromptRemote: true,
|
||||
Rename: true,
|
||||
Organization: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -104,6 +108,16 @@ func TestNewCmdFork(t *testing.T) {
|
|||
Rename: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "to org",
|
||||
cli: "--org batmanshome",
|
||||
wants: ForkOptions{
|
||||
RemoteName: "origin",
|
||||
Remote: false,
|
||||
Rename: false,
|
||||
Organization: "batmanshome",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -141,6 +155,7 @@ func TestNewCmdFork(t *testing.T) {
|
|||
assert.Equal(t, tt.wants.Remote, gotOpts.Remote)
|
||||
assert.Equal(t, tt.wants.PromptRemote, gotOpts.PromptRemote)
|
||||
assert.Equal(t, tt.wants.PromptClone, gotOpts.PromptClone)
|
||||
assert.Equal(t, tt.wants.Organization, gotOpts.Organization)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -289,6 +304,7 @@ func TestRepoFork_in_parent_tty(t *testing.T) {
|
|||
assert.Equal(t, "✓ Created fork someone/REPO\n✓ Added remote origin\n", output.Stderr())
|
||||
reg.Verify(t)
|
||||
}
|
||||
|
||||
func TestRepoFork_in_parent_nontty(t *testing.T) {
|
||||
defer stubSince(2 * time.Second)()
|
||||
reg := &httpmock.Registry{}
|
||||
|
|
@ -409,37 +425,65 @@ func TestRepoFork_in_parent(t *testing.T) {
|
|||
|
||||
func TestRepoFork_outside(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args string
|
||||
name string
|
||||
args string
|
||||
postBody string
|
||||
responseBody string
|
||||
wantStderr string
|
||||
}{
|
||||
{
|
||||
name: "url arg",
|
||||
args: "--clone=false http://github.com/OWNER/REPO.git",
|
||||
name: "url arg",
|
||||
args: "--clone=false http://github.com/OWNER/REPO.git",
|
||||
postBody: "{}\n",
|
||||
responseBody: `{"name":"REPO", "owner":{"login":"monalisa"}}`,
|
||||
wantStderr: heredoc.Doc(`
|
||||
✓ Created fork monalisa/REPO
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "full name arg",
|
||||
args: "--clone=false OWNER/REPO",
|
||||
name: "full name arg",
|
||||
args: "--clone=false OWNER/REPO",
|
||||
postBody: "{}\n",
|
||||
responseBody: `{"name":"REPO", "owner":{"login":"monalisa"}}`,
|
||||
wantStderr: heredoc.Doc(`
|
||||
✓ Created fork monalisa/REPO
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "fork to org without clone",
|
||||
args: "--clone=false OWNER/REPO --org batmanshome",
|
||||
postBody: "{\"organization\":\"batmanshome\"}\n",
|
||||
responseBody: `{"name":"REPO", "owner":{"login":"BatmansHome"}}`,
|
||||
wantStderr: heredoc.Doc(`
|
||||
✓ Created fork BatmansHome/REPO
|
||||
`),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer stubSince(2 * time.Second)()
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
reg.Register(
|
||||
httpmock.REST("POST", "repos/OWNER/REPO/forks"),
|
||||
func(req *http.Request) (*http.Response, error) {
|
||||
bb, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
assert.Equal(t, tt.postBody, string(bb))
|
||||
return &http.Response{
|
||||
Request: req,
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(bytes.NewBufferString(tt.responseBody)),
|
||||
}, nil
|
||||
})
|
||||
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
output, err := runCommand(httpClient, nil, true, tt.args)
|
||||
if err != nil {
|
||||
t.Errorf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
r := regexp.MustCompile(`Created fork.*someone/REPO`)
|
||||
if !r.MatchString(output.Stderr()) {
|
||||
t.Errorf("output did not match regexp /%s/\n> output\n%s\n", r, output)
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tt.wantStderr, output.Stderr())
|
||||
reg.Verify(t)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +1,18 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/internal/ghinstance"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/pkg/githubsearch"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/shurcooL/graphql"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
NameWithOwner string
|
||||
Description string
|
||||
IsFork bool
|
||||
IsPrivate bool
|
||||
IsArchived bool
|
||||
PushedAt time.Time
|
||||
}
|
||||
|
||||
func (r Repository) Info() string {
|
||||
var tags []string
|
||||
|
||||
if r.IsPrivate {
|
||||
tags = append(tags, "private")
|
||||
} else {
|
||||
tags = append(tags, "public")
|
||||
}
|
||||
if r.IsFork {
|
||||
tags = append(tags, "fork")
|
||||
}
|
||||
if r.IsArchived {
|
||||
tags = append(tags, "archived")
|
||||
}
|
||||
|
||||
return strings.Join(tags, ", ")
|
||||
}
|
||||
|
||||
type RepositoryList struct {
|
||||
Owner string
|
||||
Repositories []Repository
|
||||
Repositories []api.Repository
|
||||
TotalCount int
|
||||
FromSearch bool
|
||||
}
|
||||
|
|
@ -54,6 +24,7 @@ type FilterOptions struct {
|
|||
Language string
|
||||
Archived bool
|
||||
NonArchived bool
|
||||
Fields []string
|
||||
}
|
||||
|
||||
func listRepos(client *http.Client, hostname string, limit int, owner string, filter FilterOptions) (*RepositoryList, error) {
|
||||
|
|
@ -67,62 +38,65 @@ func listRepos(client *http.Client, hostname string, limit int, owner string, fi
|
|||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"perPage": githubv4.Int(perPage),
|
||||
"endCursor": (*githubv4.String)(nil),
|
||||
"perPage": githubv4.Int(perPage),
|
||||
}
|
||||
|
||||
if filter.Visibility != "" {
|
||||
variables["privacy"] = githubv4.RepositoryPrivacy(strings.ToUpper(filter.Visibility))
|
||||
} else {
|
||||
variables["privacy"] = (*githubv4.RepositoryPrivacy)(nil)
|
||||
}
|
||||
|
||||
if filter.Fork {
|
||||
variables["fork"] = githubv4.Boolean(true)
|
||||
} else if filter.Source {
|
||||
variables["fork"] = githubv4.Boolean(false)
|
||||
} else {
|
||||
variables["fork"] = (*githubv4.Boolean)(nil)
|
||||
}
|
||||
|
||||
inputs := []string{"$perPage:Int!", "$endCursor:String", "$privacy:RepositoryPrivacy", "$fork:Boolean"}
|
||||
var ownerConnection string
|
||||
if owner == "" {
|
||||
ownerConnection = `graphql:"repositoryOwner: viewer"`
|
||||
ownerConnection = "repositoryOwner: viewer"
|
||||
} else {
|
||||
ownerConnection = `graphql:"repositoryOwner(login: $owner)"`
|
||||
ownerConnection = "repositoryOwner(login: $owner)"
|
||||
variables["owner"] = githubv4.String(owner)
|
||||
inputs = append(inputs, "$owner:String!")
|
||||
}
|
||||
|
||||
type repositoryOwner struct {
|
||||
Login string
|
||||
Repositories struct {
|
||||
Nodes []Repository
|
||||
TotalCount int
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
type result struct {
|
||||
RepositoryOwner struct {
|
||||
Login string
|
||||
Repositories struct {
|
||||
Nodes []api.Repository
|
||||
TotalCount int
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
}
|
||||
}
|
||||
} `graphql:"repositories(first: $perPage, after: $endCursor, privacy: $privacy, isFork: $fork, ownerAffiliations: OWNER, orderBy: { field: PUSHED_AT, direction: DESC })"`
|
||||
}
|
||||
}
|
||||
query := reflect.StructOf([]reflect.StructField{
|
||||
{
|
||||
Name: "RepositoryOwner",
|
||||
Type: reflect.TypeOf(repositoryOwner{}),
|
||||
Tag: reflect.StructTag(ownerConnection),
|
||||
},
|
||||
})
|
||||
|
||||
gql := graphql.NewClient(ghinstance.GraphQLEndpoint(hostname), client)
|
||||
query := fmt.Sprintf(`query RepositoryList(%s) {
|
||||
%s {
|
||||
login
|
||||
repositories(first: $perPage, after: $endCursor, privacy: $privacy, isFork: $fork, ownerAffiliations: OWNER, orderBy: { field: PUSHED_AT, direction: DESC }) {
|
||||
nodes{%s}
|
||||
totalCount
|
||||
pageInfo{hasNextPage,endCursor}
|
||||
}
|
||||
}
|
||||
}`, strings.Join(inputs, ","), ownerConnection, api.RepositoryGraphQL(filter.Fields))
|
||||
|
||||
apiClient := api.NewClientFromHTTP(client)
|
||||
listResult := RepositoryList{}
|
||||
pagination:
|
||||
for {
|
||||
result := reflect.New(query)
|
||||
err := gql.QueryNamed(context.Background(), "RepositoryList", result.Interface(), variables)
|
||||
var res result
|
||||
err := apiClient.GraphQL(hostname, query, variables, &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
owner := result.Elem().FieldByName("RepositoryOwner").Interface().(repositoryOwner)
|
||||
owner := res.RepositoryOwner
|
||||
listResult.TotalCount = owner.Repositories.TotalCount
|
||||
listResult.Owner = owner.Login
|
||||
|
||||
|
|
@ -143,47 +117,52 @@ pagination:
|
|||
}
|
||||
|
||||
func searchRepos(client *http.Client, hostname string, limit int, owner string, filter FilterOptions) (*RepositoryList, error) {
|
||||
type query struct {
|
||||
type result struct {
|
||||
Search struct {
|
||||
RepositoryCount int
|
||||
Nodes []struct {
|
||||
Repository Repository `graphql:"...on Repository"`
|
||||
}
|
||||
PageInfo struct {
|
||||
Nodes []api.Repository
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
}
|
||||
} `graphql:"search(type: REPOSITORY, query: $query, first: $perPage, after: $endCursor)"`
|
||||
}
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`query RepositoryListSearch($query:String!,$perPage:Int!,$endCursor:String) {
|
||||
search(type: REPOSITORY, query: $query, first: $perPage, after: $endCursor) {
|
||||
repositoryCount
|
||||
nodes{...on Repository{%s}}
|
||||
pageInfo{hasNextPage,endCursor}
|
||||
}
|
||||
}`, api.RepositoryGraphQL(filter.Fields))
|
||||
|
||||
perPage := limit
|
||||
if perPage > 100 {
|
||||
perPage = 100
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"query": githubv4.String(searchQuery(owner, filter)),
|
||||
"perPage": githubv4.Int(perPage),
|
||||
"endCursor": (*githubv4.String)(nil),
|
||||
"query": githubv4.String(searchQuery(owner, filter)),
|
||||
"perPage": githubv4.Int(perPage),
|
||||
}
|
||||
|
||||
gql := graphql.NewClient(ghinstance.GraphQLEndpoint(hostname), client)
|
||||
apiClient := api.NewClientFromHTTP(client)
|
||||
listResult := RepositoryList{FromSearch: true}
|
||||
pagination:
|
||||
for {
|
||||
var result query
|
||||
err := gql.QueryNamed(context.Background(), "RepositoryListSearch", &result, variables)
|
||||
var result result
|
||||
err := apiClient.GraphQL(hostname, query, variables, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
listResult.TotalCount = result.Search.RepositoryCount
|
||||
for _, node := range result.Search.Nodes {
|
||||
if listResult.Owner == "" {
|
||||
idx := strings.IndexRune(node.Repository.NameWithOwner, '/')
|
||||
listResult.Owner = node.Repository.NameWithOwner[:idx]
|
||||
for _, repo := range result.Search.Nodes {
|
||||
if listResult.Owner == "" && repo.NameWithOwner != "" {
|
||||
idx := strings.IndexRune(repo.NameWithOwner, '/')
|
||||
listResult.Owner = repo.NameWithOwner[:idx]
|
||||
}
|
||||
listResult.Repositories = append(listResult.Repositories, node.Repository)
|
||||
listResult.Repositories = append(listResult.Repositories, repo)
|
||||
if len(listResult.Repositories) >= limit {
|
||||
break pagination
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ package list
|
|||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
|
|
@ -17,6 +19,7 @@ type ListOptions struct {
|
|||
HttpClient func() (*http.Client, error)
|
||||
Config func() (config.Config, error)
|
||||
IO *iostreams.IOStreams
|
||||
Exporter cmdutil.Exporter
|
||||
|
||||
Limit int
|
||||
Owner string
|
||||
|
|
@ -88,10 +91,13 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
cmd.Flags().StringVarP(&opts.Language, "language", "l", "", "Filter by primary coding language")
|
||||
cmd.Flags().BoolVar(&opts.Archived, "archived", false, "Show only archived repositories")
|
||||
cmd.Flags().BoolVar(&opts.NonArchived, "no-archived", false, "Omit archived repositories")
|
||||
cmdutil.AddJSONFlags(cmd, &opts.Exporter, api.RepositoryFields)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
var defaultFields = []string{"nameWithOwner", "description", "isPrivate", "isFork", "isArchived", "createdAt", "pushedAt"}
|
||||
|
||||
func listRun(opts *ListOptions) error {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
|
|
@ -105,6 +111,10 @@ func listRun(opts *ListOptions) error {
|
|||
Language: opts.Language,
|
||||
Archived: opts.Archived,
|
||||
NonArchived: opts.NonArchived,
|
||||
Fields: defaultFields,
|
||||
}
|
||||
if opts.Exporter != nil {
|
||||
filter.Fields = opts.Exporter.Fields()
|
||||
}
|
||||
|
||||
cfg, err := opts.Config()
|
||||
|
|
@ -127,27 +137,31 @@ func listRun(opts *ListOptions) error {
|
|||
}
|
||||
defer opts.IO.StopPager()
|
||||
|
||||
if opts.Exporter != nil {
|
||||
return opts.Exporter.Write(opts.IO.Out, listResult.Repositories, opts.IO.ColorEnabled())
|
||||
}
|
||||
|
||||
cs := opts.IO.ColorScheme()
|
||||
tp := utils.NewTablePrinter(opts.IO)
|
||||
now := opts.Now()
|
||||
|
||||
for _, repo := range listResult.Repositories {
|
||||
info := repo.Info()
|
||||
info := repoInfo(repo)
|
||||
infoColor := cs.Gray
|
||||
if repo.IsPrivate {
|
||||
infoColor = cs.Yellow
|
||||
}
|
||||
|
||||
t := repo.PushedAt
|
||||
// if listResult.FromSearch {
|
||||
// t = repo.UpdatedAt
|
||||
// }
|
||||
if repo.PushedAt == nil {
|
||||
t = &repo.CreatedAt
|
||||
}
|
||||
|
||||
tp.AddField(repo.NameWithOwner, nil, cs.Bold)
|
||||
tp.AddField(text.ReplaceExcessiveWhitespace(repo.Description), nil, nil)
|
||||
tp.AddField(info, nil, infoColor)
|
||||
if tp.IsTTY() {
|
||||
tp.AddField(utils.FuzzyAgoAbbr(now, t), nil, cs.Gray)
|
||||
tp.AddField(utils.FuzzyAgoAbbr(now, *t), nil, cs.Gray)
|
||||
} else {
|
||||
tp.AddField(t.Format(time.RFC3339), nil, nil)
|
||||
}
|
||||
|
|
@ -179,3 +193,21 @@ func listHeader(owner string, matchCount, totalMatchCount int, hasFilters bool)
|
|||
}
|
||||
return fmt.Sprintf("Showing %d of %d repositories in @%s%s", matchCount, totalMatchCount, owner, matchStr)
|
||||
}
|
||||
|
||||
func repoInfo(r api.Repository) string {
|
||||
var tags []string
|
||||
|
||||
if r.IsPrivate {
|
||||
tags = append(tags, "private")
|
||||
} else {
|
||||
tags = append(tags, "public")
|
||||
}
|
||||
if r.IsFork {
|
||||
tags = append(tags, "fork")
|
||||
}
|
||||
if r.IsArchived {
|
||||
tags = append(tags, "archived")
|
||||
}
|
||||
|
||||
return strings.Join(tags, ", ")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,25 @@ import (
|
|||
|
||||
var NotFoundError = errors.New("not found")
|
||||
|
||||
func fetchRepository(apiClient *api.Client, repo ghrepo.Interface, fields []string) (*api.Repository, error) {
|
||||
query := fmt.Sprintf(`query RepositoryInfo($owner: String!, $name: String!) {
|
||||
repository(owner: $owner, name: $name) {%s}
|
||||
}`, api.RepositoryGraphQL(fields))
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"owner": repo.RepoOwner(),
|
||||
"name": repo.RepoName(),
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Repository api.Repository
|
||||
}
|
||||
if err := apiClient.GraphQL(repo.RepoHost(), query, variables, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return api.InitRepoHostname(&result.Repository, repo.RepoHost()), nil
|
||||
}
|
||||
|
||||
type RepoReadme struct {
|
||||
Filename string
|
||||
Content string
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ type ViewOptions struct {
|
|||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Browser browser
|
||||
Exporter cmdutil.Exporter
|
||||
|
||||
RepoArg string
|
||||
Web bool
|
||||
|
|
@ -67,10 +68,13 @@ With '--branch', view a specific branch of the repository.`,
|
|||
|
||||
cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "Open a repository in the browser")
|
||||
cmd.Flags().StringVarP(&opts.Branch, "branch", "b", "", "View a specific branch of the repository")
|
||||
cmdutil.AddJSONFlags(cmd, &opts.Exporter, api.RepositoryFields)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
var defaultFields = []string{"name", "owner", "description"}
|
||||
|
||||
func viewRun(opts *ViewOptions) error {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
|
|
@ -101,11 +105,24 @@ func viewRun(opts *ViewOptions) error {
|
|||
}
|
||||
}
|
||||
|
||||
repo, err := api.GitHubRepo(apiClient, toView)
|
||||
var readme *RepoReadme
|
||||
fields := defaultFields
|
||||
if opts.Exporter != nil {
|
||||
fields = opts.Exporter.Fields()
|
||||
}
|
||||
|
||||
repo, err := fetchRepository(apiClient, toView, fields)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !opts.Web && opts.Exporter == nil {
|
||||
readme, err = RepositoryReadme(httpClient, toView, opts.Branch)
|
||||
if err != nil && !errors.Is(err, NotFoundError) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
openURL := generateBranchURL(toView, opts.Branch)
|
||||
if opts.Web {
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
|
|
@ -114,21 +131,17 @@ func viewRun(opts *ViewOptions) error {
|
|||
return opts.Browser.Browse(openURL)
|
||||
}
|
||||
|
||||
fullName := ghrepo.FullName(toView)
|
||||
|
||||
readme, err := RepositoryReadme(httpClient, toView, opts.Branch)
|
||||
if err != nil && err != NotFoundError {
|
||||
return err
|
||||
}
|
||||
|
||||
opts.IO.DetectTerminalTheme()
|
||||
|
||||
err = opts.IO.StartPager()
|
||||
if err != nil {
|
||||
if err := opts.IO.StartPager(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer opts.IO.StopPager()
|
||||
|
||||
if opts.Exporter != nil {
|
||||
return opts.Exporter.Write(opts.IO.Out, repo, opts.IO.ColorEnabled())
|
||||
}
|
||||
|
||||
fullName := ghrepo.FullName(toView)
|
||||
stdout := opts.IO.Out
|
||||
|
||||
if !opts.IO.IsStdoutTTY() {
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ package view
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/internal/run"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
|
|
@ -625,3 +627,51 @@ func Test_ViewRun_HandlesSpecialCharacters(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_viewRun_json(t *testing.T) {
|
||||
io, _, stdout, stderr := iostreams.Test()
|
||||
io.SetStdoutTTY(false)
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
reg.StubRepoInfoResponse("OWNER", "REPO", "main")
|
||||
|
||||
opts := &ViewOptions{
|
||||
IO: io,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
Exporter: &testExporter{
|
||||
fields: []string{"name", "defaultBranchRef"},
|
||||
},
|
||||
}
|
||||
|
||||
_, teardown := run.Stub()
|
||||
defer teardown(t)
|
||||
|
||||
err := viewRun(opts)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, heredoc.Doc(`
|
||||
name: REPO
|
||||
defaultBranchRef: main
|
||||
`), stdout.String())
|
||||
assert.Equal(t, "", stderr.String())
|
||||
}
|
||||
|
||||
type testExporter struct {
|
||||
fields []string
|
||||
}
|
||||
|
||||
func (e *testExporter) Fields() []string {
|
||||
return e.fields
|
||||
}
|
||||
|
||||
func (e *testExporter) Write(w io.Writer, data interface{}, colorize bool) error {
|
||||
r := data.(*api.Repository)
|
||||
fmt.Fprintf(w, "name: %s\n", r.Name)
|
||||
fmt.Fprintf(w, "defaultBranchRef: %s\n", r.DefaultBranchRef.Name)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
|
|
@ -149,13 +148,13 @@ func TestNewCmdRun(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_magicFieldValue(t *testing.T) {
|
||||
f, err := ioutil.TempFile("", "gh-test")
|
||||
f, err := ioutil.TempFile(t.TempDir(), "gh-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fmt.Fprint(f, "file contents")
|
||||
f.Close()
|
||||
t.Cleanup(func() { os.Remove(f.Name()) })
|
||||
|
||||
io, _, _, _ := iostreams.Test()
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
|
|
@ -26,6 +27,21 @@ func AddJSONFlags(cmd *cobra.Command, exportTarget *Exporter, fields []string) {
|
|||
f.StringP("jq", "q", "", "Filter JSON output using a jq `expression`")
|
||||
f.StringP("template", "t", "", "Format JSON output using a Go template")
|
||||
|
||||
_ = cmd.RegisterFlagCompletionFunc("json", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
var results []string
|
||||
if idx := strings.IndexRune(toComplete, ','); idx >= 0 {
|
||||
toComplete = toComplete[idx+1:]
|
||||
}
|
||||
toComplete = strings.ToLower(toComplete)
|
||||
for _, f := range fields {
|
||||
if strings.HasPrefix(strings.ToLower(f), toComplete) {
|
||||
results = append(results, f)
|
||||
}
|
||||
}
|
||||
sort.Strings(results)
|
||||
return results, cobra.ShellCompDirectiveNoSpace
|
||||
})
|
||||
|
||||
oldPreRun := cmd.PreRunE
|
||||
cmd.PreRunE = func(c *cobra.Command, args []string) error {
|
||||
if oldPreRun != nil {
|
||||
|
|
@ -102,11 +118,14 @@ func (e *exportFormat) Fields() []string {
|
|||
return e.fields
|
||||
}
|
||||
|
||||
// Write serializes data into JSON output written to w. If the object passed as data implements exportable,
|
||||
// or if data is a map or slice of exportable object, ExportData() will be called on each object to obtain
|
||||
// raw data for serialization.
|
||||
func (e *exportFormat) Write(w io.Writer, data interface{}, colorEnabled bool) error {
|
||||
buf := bytes.Buffer{}
|
||||
encoder := json.NewEncoder(&buf)
|
||||
encoder.SetEscapeHTML(false)
|
||||
if err := encoder.Encode(data); err != nil {
|
||||
if err := encoder.Encode(e.exportData(reflect.ValueOf(data))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -121,3 +140,44 @@ func (e *exportFormat) Write(w io.Writer, data interface{}, colorEnabled bool) e
|
|||
_, err := io.Copy(w, &buf)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *exportFormat) exportData(v reflect.Value) interface{} {
|
||||
switch v.Kind() {
|
||||
case reflect.Ptr, reflect.Interface:
|
||||
if !v.IsNil() {
|
||||
return e.exportData(v.Elem())
|
||||
}
|
||||
case reflect.Slice:
|
||||
a := make([]interface{}, v.Len())
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
a[i] = e.exportData(v.Index(i))
|
||||
}
|
||||
return a
|
||||
case reflect.Map:
|
||||
t := reflect.MapOf(v.Type().Key(), emptyInterfaceType)
|
||||
m := reflect.MakeMapWithSize(t, v.Len())
|
||||
iter := v.MapRange()
|
||||
for iter.Next() {
|
||||
ve := reflect.ValueOf(e.exportData(iter.Value()))
|
||||
m.SetMapIndex(iter.Key(), ve)
|
||||
}
|
||||
return m.Interface()
|
||||
case reflect.Struct:
|
||||
if v.CanAddr() && reflect.PtrTo(v.Type()).Implements(exportableType) {
|
||||
ve := v.Addr().Interface().(exportable)
|
||||
return ve.ExportData(e.fields)
|
||||
} else if v.Type().Implements(exportableType) {
|
||||
ve := v.Interface().(exportable)
|
||||
return ve.ExportData(e.fields)
|
||||
}
|
||||
}
|
||||
return v.Interface()
|
||||
}
|
||||
|
||||
type exportable interface {
|
||||
ExportData([]string) *map[string]interface{}
|
||||
}
|
||||
|
||||
var exportableType = reflect.TypeOf((*exportable)(nil)).Elem()
|
||||
var sliceOfEmptyInterface []interface{}
|
||||
var emptyInterfaceType = reflect.TypeOf(sliceOfEmptyInterface).Elem()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package cmdutil
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
|
|
@ -137,6 +138,29 @@ func Test_exportFormat_Write(t *testing.T) {
|
|||
wantW: "{\"name\":\"hubot\"}\n",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "call ExportData",
|
||||
exporter: exportFormat{fields: []string{"field1", "field2"}},
|
||||
args: args{
|
||||
data: &exportableItem{"item1"},
|
||||
colorEnabled: false,
|
||||
},
|
||||
wantW: "{\"field1\":\"item1:field1\",\"field2\":\"item1:field2\"}\n",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "recursively call ExportData",
|
||||
exporter: exportFormat{fields: []string{"f1", "f2"}},
|
||||
args: args{
|
||||
data: map[string]interface{}{
|
||||
"s1": []exportableItem{{"i1"}, {"i2"}},
|
||||
"s2": []exportableItem{{"i3"}},
|
||||
},
|
||||
colorEnabled: false,
|
||||
},
|
||||
wantW: "{\"s1\":[{\"f1\":\"i1:f1\",\"f2\":\"i1:f2\"},{\"f1\":\"i2:f1\",\"f2\":\"i2:f2\"}],\"s2\":[{\"f1\":\"i3:f1\",\"f2\":\"i3:f2\"}]}\n",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "with jq filter",
|
||||
exporter: exportFormat{filter: ".name"},
|
||||
|
|
@ -166,8 +190,20 @@ func Test_exportFormat_Write(t *testing.T) {
|
|||
return
|
||||
}
|
||||
if gotW := w.String(); gotW != tt.wantW {
|
||||
t.Errorf("exportFormat.Write() = %v, want %v", gotW, tt.wantW)
|
||||
t.Errorf("exportFormat.Write() = %q, want %q", gotW, tt.wantW)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type exportableItem struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (e *exportableItem) ExportData(fields []string) *map[string]interface{} {
|
||||
m := map[string]interface{}{}
|
||||
for _, f := range fields {
|
||||
m[f] = fmt.Sprintf("%s:%s", e.Name, f)
|
||||
}
|
||||
return &m
|
||||
}
|
||||
|
|
|
|||
|
|
@ -261,12 +261,11 @@ func TestFindLegacy(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExtractName(t *testing.T) {
|
||||
tmpfile, err := ioutil.TempFile("", "gh-cli")
|
||||
tmpfile, err := ioutil.TempFile(t.TempDir(), "gh-cli")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tmpfile.Close()
|
||||
defer os.Remove(tmpfile.Name())
|
||||
defer tmpfile.Close()
|
||||
|
||||
type args struct {
|
||||
filePath string
|
||||
|
|
@ -322,12 +321,11 @@ about: This is how you report bugs
|
|||
}
|
||||
|
||||
func TestExtractContents(t *testing.T) {
|
||||
tmpfile, err := ioutil.TempFile("", "gh-cli")
|
||||
tmpfile, err := ioutil.TempFile(t.TempDir(), "gh-cli")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tmpfile.Close()
|
||||
defer os.Remove(tmpfile.Name())
|
||||
defer tmpfile.Close()
|
||||
|
||||
type args struct {
|
||||
filePath string
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import (
|
|||
const (
|
||||
colorDelim = "1;38" // bright white
|
||||
colorKey = "1;34" // bright blue
|
||||
colorNull = "1;30" // gray
|
||||
colorNull = "36" // cyan
|
||||
colorString = "32" // green
|
||||
colorBool = "33" // yellow
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue