Merge remote-tracking branch 'origin' into ghe-api
This commit is contained in:
commit
0cbcf8a7fa
19 changed files with 1251 additions and 742 deletions
|
|
@ -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
128
pkg/cmd/repo/clone/clone.go
Normal 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
|
||||
}
|
||||
195
pkg/cmd/repo/clone/clone_test.go
Normal file
195
pkg/cmd/repo/clone/clone_test.go
Normal 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
45
pkg/cmd/repo/view/http.go
Normal 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
188
pkg/cmd/repo/view/view.go
Normal 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")
|
||||
}
|
||||
471
pkg/cmd/repo/view/view_test.go
Normal file
471
pkg/cmd/repo/view/view_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue