Merge remote-tracking branch 'origin' into ghe-api

This commit is contained in:
Mislav Marohnić 2020-07-27 16:29:13 +02:00
commit 0cbcf8a7fa
19 changed files with 1251 additions and 742 deletions

View file

@ -71,8 +71,6 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
return nil
},
RunE: func(c *cobra.Command, args []string) error {
opts.HttpClient = f.HttpClient
opts.Filenames = args
if runF != nil {

128
pkg/cmd/repo/clone/clone.go Normal file
View file

@ -0,0 +1,128 @@
package clone
import (
"net/http"
"strings"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/api"
"github.com/cli/cli/git"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/internal/ghinstance"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/iostreams"
"github.com/spf13/cobra"
)
type CloneOptions struct {
HttpClient func() (*http.Client, error)
Config func() (config.Config, error)
IO *iostreams.IOStreams
GitArgs []string
Directory string
Repository string
}
func NewCmdClone(f *cmdutil.Factory, runF func(*CloneOptions) error) *cobra.Command {
opts := &CloneOptions{
IO: f.IOStreams,
HttpClient: f.HttpClient,
Config: f.Config,
}
cmd := &cobra.Command{
Use: "clone <repository> [<directory>]",
Args: cobra.MinimumNArgs(1),
Short: "Clone a repository locally",
Long: heredoc.Doc(
`Clone a GitHub repository locally.
If the "OWNER/" portion of the "OWNER/REPO" repository argument is omitted, it
defaults to the name of the authenticating user.
To pass 'git clone' flags, separate them with '--'.
`),
RunE: func(cmd *cobra.Command, args []string) error {
opts.Repository = args[0]
opts.GitArgs = args[1:]
if runF != nil {
return runF(opts)
}
return cloneRun(opts)
},
}
return cmd
}
func cloneRun(opts *CloneOptions) error {
httpClient, err := opts.HttpClient()
if err != nil {
return err
}
// TODO This is overly wordy and I'd like to streamline this.
cfg, err := opts.Config()
if err != nil {
return err
}
protocol, err := cfg.Get("", "git_protocol")
if err != nil {
return err
}
apiClient := api.NewClientFromHTTP(httpClient)
cloneURL := opts.Repository
if !strings.Contains(cloneURL, ":") {
if !strings.Contains(cloneURL, "/") {
// TODO: GHE compat
currentUser, err := api.CurrentLoginName(apiClient, ghinstance.Default())
if err != nil {
return err
}
cloneURL = currentUser + "/" + cloneURL
}
repo, err := ghrepo.FromFullName(cloneURL)
if err != nil {
return err
}
cloneURL = ghrepo.FormatRemoteURL(repo, protocol)
}
var repo ghrepo.Interface
var parentRepo ghrepo.Interface
// TODO: consider caching and reusing `git.ParseSSHConfig().Translator()`
// here to handle hostname aliases in SSH remotes
if u, err := git.ParseURL(cloneURL); err == nil {
repo, _ = ghrepo.FromURL(u)
}
if repo != nil {
parentRepo, err = api.RepoParent(apiClient, repo)
if err != nil {
return err
}
}
cloneDir, err := git.RunClone(cloneURL, opts.GitArgs)
if err != nil {
return err
}
if parentRepo != nil {
upstreamURL := ghrepo.FormatRemoteURL(parentRepo, protocol)
err := git.AddUpstreamRemote(upstreamURL, cloneDir)
if err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,195 @@
package clone
import (
"bytes"
"net/http"
"strings"
"testing"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/httpmock"
"github.com/cli/cli/pkg/iostreams"
"github.com/cli/cli/test"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
)
// TODO copypasta from command package
type cmdOut struct {
outBuf, errBuf *bytes.Buffer
}
func (c cmdOut) String() string {
return c.outBuf.String()
}
func (c cmdOut) Stderr() string {
return c.errBuf.String()
}
func runCloneCommand(httpClient *http.Client, cli string) (*cmdOut, error) {
io, stdin, stdout, stderr := iostreams.Test()
fac := &cmdutil.Factory{
IOStreams: io,
HttpClient: func() (*http.Client, error) {
return httpClient, nil
},
Config: func() (config.Config, error) {
return config.NewBlankConfig(), nil
},
}
cmd := NewCmdClone(fac, nil)
argv, err := shlex.Split(cli)
cmd.SetArgs(argv)
cmd.SetIn(stdin)
cmd.SetOut(stdout)
cmd.SetErr(stderr)
if err != nil {
panic(err)
}
_, err = cmd.ExecuteC()
if err != nil {
return nil, err
}
return &cmdOut{stdout, stderr}, nil
}
func Test_RepoClone(t *testing.T) {
tests := []struct {
name string
args string
want string
}{
{
name: "shorthand",
args: "OWNER/REPO",
want: "git clone https://github.com/OWNER/REPO.git",
},
{
name: "shorthand with directory",
args: "OWNER/REPO target_directory",
want: "git clone https://github.com/OWNER/REPO.git target_directory",
},
{
name: "clone arguments",
args: "OWNER/REPO -- -o upstream --depth 1",
want: "git clone -o upstream --depth 1 https://github.com/OWNER/REPO.git",
},
{
name: "clone arguments with directory",
args: "OWNER/REPO target_directory -- -o upstream --depth 1",
want: "git clone -o upstream --depth 1 https://github.com/OWNER/REPO.git target_directory",
},
{
name: "HTTPS URL",
args: "https://github.com/OWNER/REPO",
want: "git clone https://github.com/OWNER/REPO",
},
{
name: "SSH URL",
args: "git@github.com:OWNER/REPO.git",
want: "git clone git@github.com:OWNER/REPO.git",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reg := &httpmock.Registry{}
reg.Register(
httpmock.GraphQL(`query RepositoryFindParent\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"parent": null
} } }
`))
httpClient := &http.Client{Transport: reg}
cs, restore := test.InitCmdStubber()
defer restore()
cs.Stub("") // git clone
output, err := runCloneCommand(httpClient, tt.args)
if err != nil {
t.Fatalf("error running command `repo clone`: %v", err)
}
assert.Equal(t, "", output.String())
assert.Equal(t, "", output.Stderr())
assert.Equal(t, 1, cs.Count)
assert.Equal(t, tt.want, strings.Join(cs.Calls[0].Args, " "))
reg.Verify(t)
})
}
}
func Test_RepoClone_hasParent(t *testing.T) {
reg := &httpmock.Registry{}
reg.Register(
httpmock.GraphQL(`query RepositoryFindParent\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"parent": {
"owner": {"login": "hubot"},
"name": "ORIG"
}
} } }
`))
httpClient := &http.Client{Transport: reg}
cs, restore := test.InitCmdStubber()
defer restore()
cs.Stub("") // git clone
cs.Stub("") // git remote add
_, err := runCloneCommand(httpClient, "OWNER/REPO")
if err != nil {
t.Fatalf("error running command `repo clone`: %v", err)
}
assert.Equal(t, 2, cs.Count)
assert.Equal(t, "git -C REPO remote add -f upstream https://github.com/hubot/ORIG.git", strings.Join(cs.Calls[1].Args, " "))
}
func Test_RepoClone_withoutUsername(t *testing.T) {
reg := &httpmock.Registry{}
reg.Register(
httpmock.GraphQL(`query UserCurrent\b`),
httpmock.StringResponse(`
{ "data": { "viewer": {
"login": "OWNER"
}}}`))
reg.Register(
httpmock.GraphQL(`query RepositoryFindParent\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"parent": null
} } }`))
httpClient := &http.Client{Transport: reg}
cs, restore := test.InitCmdStubber()
defer restore()
cs.Stub("") // git clone
output, err := runCloneCommand(httpClient, "REPO")
if err != nil {
t.Fatalf("error running command `repo clone`: %v", err)
}
assert.Equal(t, "", output.String())
assert.Equal(t, "", output.Stderr())
assert.Equal(t, 1, cs.Count)
assert.Equal(t, "git clone https://github.com/OWNER/REPO.git", strings.Join(cs.Calls[0].Args, " "))
}

45
pkg/cmd/repo/view/http.go Normal file
View file

@ -0,0 +1,45 @@
package view
import (
"encoding/base64"
"errors"
"fmt"
"net/http"
"github.com/cli/cli/api"
"github.com/cli/cli/internal/ghrepo"
)
var NotFoundError = errors.New("not found")
type RepoReadme struct {
Filename string
Content string
}
func RepositoryReadme(client *http.Client, repo ghrepo.Interface) (*RepoReadme, error) {
apiClient := api.NewClientFromHTTP(client)
var response struct {
Name string
Content string
}
err := apiClient.REST(repo.RepoHost(), "GET", fmt.Sprintf("repos/%s/readme", ghrepo.FullName(repo)), nil, &response)
if err != nil {
var httpError api.HTTPError
if errors.As(err, &httpError) && httpError.StatusCode == 404 {
return nil, NotFoundError
}
return nil, err
}
decoded, err := base64.StdEncoding.DecodeString(response.Content)
if err != nil {
return nil, fmt.Errorf("failed to decode readme: %w", err)
}
return &RepoReadme{
Filename: response.Name,
Content: string(decoded),
}, nil
}

188
pkg/cmd/repo/view/view.go Normal file
View file

@ -0,0 +1,188 @@
package view
import (
"fmt"
"html/template"
"net/http"
"net/url"
"strings"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/api"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/iostreams"
"github.com/cli/cli/utils"
"github.com/spf13/cobra"
)
type ViewOptions struct {
HttpClient func() (*http.Client, error)
IO *iostreams.IOStreams
BaseRepo func() (ghrepo.Interface, error)
RepoArg string
Web bool
}
func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Command {
opts := ViewOptions{
IO: f.IOStreams,
HttpClient: f.HttpClient,
BaseRepo: f.BaseRepo,
}
cmd := &cobra.Command{
Use: "view [<repository>]",
Short: "View a repository",
Long: `Display the description and the README of a GitHub repository.
With no argument, the repository for the current directory is displayed.
With '--web', open the repository in a web browser instead.`,
Args: cobra.MaximumNArgs(1),
RunE: func(c *cobra.Command, args []string) error {
if len(args) > 0 {
opts.RepoArg = args[0]
}
if runF != nil {
return runF(&opts)
}
return viewRun(&opts)
},
}
cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "Open a repository in the browser")
return cmd
}
func viewRun(opts *ViewOptions) error {
httpClient, err := opts.HttpClient()
if err != nil {
return err
}
var toView ghrepo.Interface
if opts.RepoArg == "" {
var err error
toView, err = opts.BaseRepo()
if err != nil {
return err
}
} else {
if utils.IsURL(opts.RepoArg) {
parsedURL, err := url.Parse(opts.RepoArg)
if err != nil {
return fmt.Errorf("did not understand argument: %w", err)
}
toView, err = ghrepo.FromURL(parsedURL)
if err != nil {
return fmt.Errorf("did not understand argument: %w", err)
}
} else {
var err error
toView, err = ghrepo.FromFullName(opts.RepoArg)
if err != nil {
return fmt.Errorf("argument error: %w", err)
}
}
}
apiClient := api.NewClientFromHTTP(httpClient)
repo, err := api.GitHubRepo(apiClient, toView)
if err != nil {
return err
}
openURL := ghrepo.GenerateRepoURL(toView, "")
if opts.Web {
if opts.IO.IsStdoutTTY() {
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
}
return utils.OpenInBrowser(openURL)
}
fullName := ghrepo.FullName(toView)
readme, err := RepositoryReadme(httpClient, toView)
if err != nil && err != NotFoundError {
return err
}
stdout := opts.IO.Out
if !opts.IO.IsStdoutTTY() {
fmt.Fprintf(stdout, "name:\t%s\n", fullName)
fmt.Fprintf(stdout, "description:\t%s\n", repo.Description)
if readme != nil {
fmt.Fprintln(stdout, "--")
fmt.Fprintf(stdout, readme.Content)
fmt.Fprintln(stdout)
}
return nil
}
repoTmpl := heredoc.Doc(`
{{.FullName}}
{{.Description}}
{{.Readme}}
{{.View}}
`)
tmpl, err := template.New("repo").Parse(repoTmpl)
if err != nil {
return err
}
var readmeContent string
if readme == nil {
readmeContent = utils.Gray("This repository does not have a README")
} else if isMarkdownFile(readme.Filename) {
var err error
readmeContent, err = utils.RenderMarkdown(readme.Content)
if err != nil {
return fmt.Errorf("error rendering markdown: %w", err)
}
} else {
readmeContent = readme.Content
}
description := repo.Description
if description == "" {
description = utils.Gray("No description provided")
}
repoData := struct {
FullName string
Description string
Readme string
View string
}{
FullName: utils.Bold(fullName),
Description: description,
Readme: readmeContent,
View: utils.Gray(fmt.Sprintf("View this repository on GitHub: %s", openURL)),
}
err = tmpl.Execute(stdout, repoData)
if err != nil {
return err
}
return nil
}
func isMarkdownFile(filename string) bool {
// kind of gross, but i'm assuming that 90% of the time the suffix will just be .md. it didn't
// seem worth executing a regex for this given that assumption.
return strings.HasSuffix(filename, ".md") ||
strings.HasSuffix(filename, ".markdown") ||
strings.HasSuffix(filename, ".mdown") ||
strings.HasSuffix(filename, ".mkdown")
}

View file

@ -0,0 +1,471 @@
package view
import (
"bytes"
"fmt"
"net/http"
"testing"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/httpmock"
"github.com/cli/cli/pkg/iostreams"
"github.com/cli/cli/test"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
)
func TestNewCmdView(t *testing.T) {
tests := []struct {
name string
cli string
wants ViewOptions
wantsErr bool
}{
{
name: "no args",
cli: "",
wants: ViewOptions{
RepoArg: "",
Web: false,
},
},
{
name: "sets repo arg",
cli: "some/repo",
wants: ViewOptions{
RepoArg: "some/repo",
Web: false,
},
},
{
name: "sets web",
cli: "-w",
wants: ViewOptions{
RepoArg: "",
Web: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
io, _, _, _ := iostreams.Test()
f := &cmdutil.Factory{
IOStreams: io,
}
// THOUGHT: this seems ripe for cmdutil. It's almost identical to the set up for the same test
// in gist create.
argv, err := shlex.Split(tt.cli)
assert.NoError(t, err)
var gotOpts *ViewOptions
cmd := NewCmdView(f, func(opts *ViewOptions) 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.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.wants.Web, gotOpts.Web)
assert.Equal(t, tt.wants.RepoArg, gotOpts.RepoArg)
})
}
}
func Test_RepoView_Web(t *testing.T) {
tests := []struct {
name string
stdoutTTY bool
wantStderr string
}{
{
name: "tty",
stdoutTTY: true,
wantStderr: "Opening github.com/OWNER/REPO in your browser.\n",
},
{
name: "nontty",
wantStderr: "",
},
}
for _, tt := range tests {
reg := &httpmock.Registry{}
reg.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`{}`))
opts := &ViewOptions{
Web: true,
HttpClient: func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
},
BaseRepo: func() (ghrepo.Interface, error) {
return ghrepo.New("OWNER", "REPO"), nil
},
}
io, _, stdout, stderr := iostreams.Test()
opts.IO = io
t.Run(tt.name, func(t *testing.T) {
io.SetStdoutTTY(tt.stdoutTTY)
cs, teardown := test.InitCmdStubber()
defer teardown()
cs.Stub("") // browser open
if err := viewRun(opts); err != nil {
t.Errorf("viewRun() error = %v", err)
}
assert.Equal(t, "", stdout.String())
assert.Equal(t, 1, len(cs.Calls))
call := cs.Calls[0]
assert.Equal(t, "https://github.com/OWNER/REPO", call.Args[len(call.Args)-1])
assert.Equal(t, tt.wantStderr, stderr.String())
reg.Verify(t)
})
}
}
func Test_ViewRun(t *testing.T) {
tests := []struct {
name string
opts *ViewOptions
repoName string
stdoutTTY bool
wantOut string
wantStderr string
wantErr bool
}{
{
name: "nontty",
wantOut: heredoc.Doc(`
name: OWNER/REPO
description: social distancing
--
# truly cool readme check it out
`),
},
{
name: "url arg",
repoName: "jill/valentine",
opts: &ViewOptions{
RepoArg: "https://github.com/jill/valentine",
},
stdoutTTY: true,
wantOut: heredoc.Doc(`
jill/valentine
social distancing
# truly cool readme check it out
View this repository on GitHub: https://github.com/jill/valentine
`),
},
{
name: "name arg",
repoName: "jill/valentine",
opts: &ViewOptions{
RepoArg: "jill/valentine",
},
stdoutTTY: true,
wantOut: heredoc.Doc(`
jill/valentine
social distancing
# truly cool readme check it out
View this repository on GitHub: https://github.com/jill/valentine
`),
},
{
name: "no args",
stdoutTTY: true,
wantOut: heredoc.Doc(`
OWNER/REPO
social distancing
# truly cool readme check it out
View this repository on GitHub: https://github.com/OWNER/REPO
`),
},
}
for _, tt := range tests {
if tt.opts == nil {
tt.opts = &ViewOptions{}
}
if tt.repoName == "" {
tt.repoName = "OWNER/REPO"
}
tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
repo, _ := ghrepo.FromFullName(tt.repoName)
return repo, nil
}
reg := &httpmock.Registry{}
reg.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": {
"repository": {
"description": "social distancing"
} } }`))
reg.Register(
httpmock.REST("GET", fmt.Sprintf("repos/%s/readme", tt.repoName)),
httpmock.StringResponse(`
{ "name": "readme.md",
"content": "IyB0cnVseSBjb29sIHJlYWRtZSBjaGVjayBpdCBvdXQ="}`))
tt.opts.HttpClient = func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
}
io, _, stdout, stderr := iostreams.Test()
tt.opts.IO = io
t.Run(tt.name, func(t *testing.T) {
io.SetStdoutTTY(tt.stdoutTTY)
if err := viewRun(tt.opts); (err != nil) != tt.wantErr {
t.Errorf("viewRun() error = %v, wantErr %v", err, tt.wantErr)
}
assert.Equal(t, tt.wantStderr, stderr.String())
assert.Equal(t, tt.wantOut, stdout.String())
reg.Verify(t)
})
}
}
func Test_ViewRun_NonMarkdownReadme(t *testing.T) {
tests := []struct {
name string
stdoutTTY bool
wantOut string
}{
{
name: "tty",
wantOut: heredoc.Doc(`
OWNER/REPO
social distancing
# truly cool readme check it out
View this repository on GitHub: https://github.com/OWNER/REPO
`),
stdoutTTY: true,
},
{
name: "nontty",
wantOut: heredoc.Doc(`
name: OWNER/REPO
description: social distancing
--
# truly cool readme check it out
`),
},
}
for _, tt := range tests {
reg := &httpmock.Registry{}
reg.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": {
"repository": {
"description": "social distancing"
} } }`))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/readme"),
httpmock.StringResponse(`
{ "name": "readme.org",
"content": "IyB0cnVseSBjb29sIHJlYWRtZSBjaGVjayBpdCBvdXQ="}`))
opts := &ViewOptions{
HttpClient: func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
},
BaseRepo: func() (ghrepo.Interface, error) {
return ghrepo.New("OWNER", "REPO"), nil
},
}
io, _, stdout, stderr := iostreams.Test()
opts.IO = io
t.Run(tt.name, func(t *testing.T) {
io.SetStdoutTTY(tt.stdoutTTY)
if err := viewRun(opts); err != nil {
t.Errorf("viewRun() error = %v", err)
}
assert.Equal(t, tt.wantOut, stdout.String())
assert.Equal(t, "", stderr.String())
reg.Verify(t)
})
}
}
func Test_ViewRun_NoReadme(t *testing.T) {
tests := []struct {
name string
stdoutTTY bool
wantOut string
}{
{
name: "tty",
wantOut: heredoc.Doc(`
OWNER/REPO
social distancing
This repository does not have a README
View this repository on GitHub: https://github.com/OWNER/REPO
`),
stdoutTTY: true,
},
{
name: "nontty",
wantOut: heredoc.Doc(`
name: OWNER/REPO
description: social distancing
`),
},
}
for _, tt := range tests {
reg := &httpmock.Registry{}
reg.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": {
"repository": {
"description": "social distancing"
} } }`))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/readme"),
httpmock.StatusStringResponse(404, `{}`))
opts := &ViewOptions{
HttpClient: func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
},
BaseRepo: func() (ghrepo.Interface, error) {
return ghrepo.New("OWNER", "REPO"), nil
},
}
io, _, stdout, stderr := iostreams.Test()
opts.IO = io
t.Run(tt.name, func(t *testing.T) {
io.SetStdoutTTY(tt.stdoutTTY)
if err := viewRun(opts); err != nil {
t.Errorf("viewRun() error = %v", err)
}
assert.Equal(t, tt.wantOut, stdout.String())
assert.Equal(t, "", stderr.String())
reg.Verify(t)
})
}
}
func Test_ViewRun_NoDescription(t *testing.T) {
tests := []struct {
name string
stdoutTTY bool
wantOut string
}{
{
name: "tty",
wantOut: heredoc.Doc(`
OWNER/REPO
No description provided
# truly cool readme check it out
View this repository on GitHub: https://github.com/OWNER/REPO
`),
stdoutTTY: true,
},
{
name: "nontty",
wantOut: heredoc.Doc(`
name: OWNER/REPO
description:
--
# truly cool readme check it out
`),
},
}
for _, tt := range tests {
reg := &httpmock.Registry{}
reg.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": {
"repository": {
"description": ""
} } }`))
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/readme"),
httpmock.StringResponse(`
{ "name": "readme.org",
"content": "IyB0cnVseSBjb29sIHJlYWRtZSBjaGVjayBpdCBvdXQ="}`))
opts := &ViewOptions{
HttpClient: func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
},
BaseRepo: func() (ghrepo.Interface, error) {
return ghrepo.New("OWNER", "REPO"), nil
},
}
io, _, stdout, stderr := iostreams.Test()
opts.IO = io
t.Run(tt.name, func(t *testing.T) {
io.SetStdoutTTY(tt.stdoutTTY)
if err := viewRun(opts); err != nil {
t.Errorf("viewRun() error = %v", err)
}
assert.Equal(t, tt.wantOut, stdout.String())
assert.Equal(t, "", stderr.String())
reg.Verify(t)
})
}
}

View file

@ -3,6 +3,7 @@ package cmdutil
import (
"net/http"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/pkg/iostreams"
)
@ -11,4 +12,5 @@ type Factory struct {
IOStreams *iostreams.IOStreams
HttpClient func() (*http.Client, error)
BaseRepo func() (ghrepo.Interface, error)
Config func() (config.Config, error)
}

View file

@ -17,8 +17,10 @@ type IOStreams struct {
colorEnabled bool
stdinTTYOverride bool
stdinIsTTY bool
stdinTTYOverride bool
stdinIsTTY bool
stdoutTTYOverride bool
stdoutIsTTY bool
}
func (s *IOStreams) ColorEnabled() bool {
@ -40,6 +42,21 @@ func (s *IOStreams) IsStdinTTY() bool {
return false
}
func (s *IOStreams) SetStdoutTTY(isTTY bool) {
s.stdoutTTYOverride = true
s.stdoutIsTTY = isTTY
}
func (s *IOStreams) IsStdoutTTY() bool {
if s.stdoutTTYOverride {
return s.stdoutIsTTY
}
if stdout, ok := s.Out.(*os.File); ok {
return isTerminal(stdout)
}
return false
}
func System() *IOStreams {
var out io.Writer = os.Stdout
var colorEnabled bool