Merge pull request #1416 from vilmibm/migrate-repo-clone

isolate repo clone
This commit is contained in:
Nate Smith 2020-07-24 11:08:14 -05:00 committed by GitHub
commit 75120344c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1249 additions and 740 deletions

View file

@ -3,7 +3,6 @@ package api
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
@ -405,37 +404,6 @@ func RepoCreate(client *Client, input RepoCreateInput) (*Repository, error) {
return initRepoHostname(&response.CreateRepository.Repository, "github.com"), nil
}
type RepoReadme struct {
Filename string
Content string
}
func RepositoryReadme(client *Client, repo ghrepo.Interface) (*RepoReadme, error) {
var response struct {
Name string
Content string
}
err := client.REST("GET", fmt.Sprintf("repos/%s/readme", ghrepo.FullName(repo)), nil, &response)
if err != nil {
var httpError HTTPError
if errors.As(err, &httpError) && httpError.StatusCode == 404 {
return nil, &NotFoundError{err}
}
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
}
type RepoMetadataResult struct {
AssignableUsers []RepoAssignee
Labels []RepoLabel

View file

@ -227,7 +227,7 @@ func issueList(cmd *cobra.Command, args []string) error {
}
if web {
issueListURL := generateRepoURL(baseRepo, "issues")
issueListURL := ghrepo.GenerateRepoURL(baseRepo, "issues")
openURL, err := listURLWithQuery(issueListURL, filterOptions{
entity: "issue",
state: state,
@ -240,7 +240,7 @@ func issueList(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", displayURL(openURL))
fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", utils.DisplayURL(openURL))
return utils.OpenInBrowser(openURL)
}
@ -504,7 +504,7 @@ func issueCreate(cmd *cobra.Command, args []string) error {
}
if isWeb, err := cmd.Flags().GetBool("web"); err == nil && isWeb {
openURL := generateRepoURL(baseRepo, "issues/new")
openURL := ghrepo.GenerateRepoURL(baseRepo, "issues/new")
if title != "" || body != "" {
milestone := ""
if len(milestoneTitles) > 0 {
@ -518,7 +518,7 @@ func issueCreate(cmd *cobra.Command, args []string) error {
openURL += "/choose"
}
if connectedToTerminal(cmd) {
cmd.Printf("Opening %s in your browser.\n", displayURL(openURL))
cmd.Printf("Opening %s in your browser.\n", utils.DisplayURL(openURL))
}
return utils.OpenInBrowser(openURL)
}
@ -582,7 +582,7 @@ func issueCreate(cmd *cobra.Command, args []string) error {
}
if action == PreviewAction {
openURL := generateRepoURL(baseRepo, "issues/new")
openURL := ghrepo.GenerateRepoURL(baseRepo, "issues/new")
milestone := ""
if len(milestoneTitles) > 0 {
milestone = milestoneTitles[0]
@ -592,7 +592,7 @@ func issueCreate(cmd *cobra.Command, args []string) error {
return err
}
// TODO could exceed max url length for explorer
fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", displayURL(openURL))
fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", utils.DisplayURL(openURL))
return utils.OpenInBrowser(openURL)
} else if action == SubmitAction {
params := map[string]interface{}{
@ -618,14 +618,6 @@ func issueCreate(cmd *cobra.Command, args []string) error {
return nil
}
func generateRepoURL(repo ghrepo.Interface, p string, args ...interface{}) string {
baseURL := fmt.Sprintf("https://%s/%s/%s", repo.RepoHost(), repo.RepoOwner(), repo.RepoName())
if p != "" {
return baseURL + "/" + fmt.Sprintf(p, args...)
}
return baseURL
}
func addMetadataToIssueParams(client *api.Client, baseRepo ghrepo.Interface, params map[string]interface{}, tb *issueMetadataState) error {
if !tb.HasMetadata() {
return nil
@ -844,11 +836,3 @@ func issueReopen(cmd *cobra.Command, args []string) error {
return nil
}
func displayURL(urlStr string) string {
u, err := url.Parse(urlStr)
if err != nil {
return urlStr
}
return u.Hostname() + u.Path
}

View file

@ -235,7 +235,7 @@ func prList(cmd *cobra.Command, args []string) error {
}
if web {
prListURL := generateRepoURL(baseRepo, "pulls")
prListURL := ghrepo.GenerateRepoURL(baseRepo, "pulls")
openURL, err := listURLWithQuery(prListURL, filterOptions{
entity: "pr",
state: state,
@ -246,7 +246,7 @@ func prList(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", displayURL(openURL))
fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", utils.DisplayURL(openURL))
return utils.OpenInBrowser(openURL)
}

View file

@ -367,7 +367,7 @@ func prCreate(cmd *cobra.Command, _ []string) error {
}
if connectedToTerminal(cmd) {
// TODO could exceed max url length for explorer
fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", displayURL(openURL))
fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", utils.DisplayURL(openURL))
}
return utils.OpenInBrowser(openURL)
} else {
@ -447,7 +447,7 @@ func withPrAndIssueQueryParams(baseURL, title, body string, assignees, labels, p
}
func generateCompareURL(r ghrepo.Interface, base, head, title, body string, assignees, labels, projects []string, milestone string) (string, error) {
u := generateRepoURL(r, "compare/%s...%s?expand=1", base, head)
u := ghrepo.GenerateRepoURL(r, "compare/%s...%s?expand=1", base, head)
url, err := withPrAndIssueQueryParams(u, title, body, assignees, labels, projects, milestone)
if err != nil {
return "", err

View file

@ -7,7 +7,6 @@ import (
"os"
"path"
"strings"
"text/template"
"time"
"github.com/AlecAivazis/survey/v2"
@ -21,9 +20,6 @@ import (
)
func init() {
RootCmd.AddCommand(repoCmd)
repoCmd.AddCommand(repoCloneCmd)
repoCmd.AddCommand(repoCreateCmd)
repoCreateCmd.Flags().StringP("description", "d", "", "Description of repository")
repoCreateCmd.Flags().StringP("homepage", "h", "", "Repository home page URL")
@ -38,9 +34,6 @@ func init() {
repoForkCmd.Flags().Lookup("clone").NoOptDefVal = "true"
repoForkCmd.Flags().Lookup("remote").NoOptDefVal = "true"
repoCmd.AddCommand(repoViewCmd)
repoViewCmd.Flags().BoolP("web", "w", false, "Open a repository in the browser")
repoCmd.AddCommand(repoCreditsCmd)
repoCreditsCmd.Flags().BoolP("static", "s", false, "Print a static version of the credits")
}
@ -62,19 +55,6 @@ A repository can be supplied as an argument in any of the following formats:
- by URL, e.g. "https://github.com/OWNER/REPO"`},
}
var repoCloneCmd = &cobra.Command{
Use: "clone <repository> [<directory>]",
Args: cobra.MinimumNArgs(1),
Short: "Clone a repository locally",
Long: `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: repoClone,
}
var repoCreateCmd = &cobra.Command{
Use: "create [<name>]",
Short: "Create a new repository",
@ -104,17 +84,6 @@ With no argument, creates a fork of the current repository. Otherwise, forks the
RunE: repoFork,
}
var repoViewCmd = &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.`,
RunE: repoView,
}
var repoCreditsCmd = &cobra.Command{
Use: "credits [<repository>]",
Short: "View credits for a repository",
@ -136,104 +105,6 @@ var repoCreditsCmd = &cobra.Command{
Hidden: true,
}
func parseCloneArgs(extraArgs []string) (args []string, target string) {
args = extraArgs
if len(args) > 0 {
if !strings.HasPrefix(args[0], "-") {
target, args = args[0], args[1:]
}
}
return
}
func runClone(cloneURL string, args []string) (target string, err error) {
cloneArgs, target := parseCloneArgs(args)
cloneArgs = append(cloneArgs, cloneURL)
// If the args contain an explicit target, pass it to clone
// otherwise, parse the URL to determine where git cloned it to so we can return it
if target != "" {
cloneArgs = append(cloneArgs, target)
} else {
target = path.Base(strings.TrimSuffix(cloneURL, ".git"))
}
cloneArgs = append([]string{"clone"}, cloneArgs...)
cloneCmd := git.GitCommand(cloneArgs...)
cloneCmd.Stdin = os.Stdin
cloneCmd.Stdout = os.Stdout
cloneCmd.Stderr = os.Stderr
err = run.PrepareCmd(cloneCmd).Run()
return
}
func repoClone(cmd *cobra.Command, args []string) error {
ctx := contextForCommand(cmd)
apiClient, err := apiClientForContext(ctx)
if err != nil {
return err
}
cloneURL := args[0]
if !strings.Contains(cloneURL, ":") {
if !strings.Contains(cloneURL, "/") {
currentUser, err := api.CurrentLoginName(apiClient)
if err != nil {
return err
}
cloneURL = currentUser + "/" + cloneURL
}
repo, err := ghrepo.FromFullName(cloneURL)
if err != nil {
return err
}
cloneURL = formatRemoteURL(cmd, repo)
}
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 := runClone(cloneURL, args[1:])
if err != nil {
return err
}
if parentRepo != nil {
err := addUpstreamRemote(cmd, parentRepo, cloneDir)
if err != nil {
return err
}
}
return nil
}
func addUpstreamRemote(cmd *cobra.Command, parentRepo ghrepo.Interface, cloneDir string) error {
upstreamURL := formatRemoteURL(cmd, parentRepo)
cloneCmd := git.GitCommand("-C", cloneDir, "remote", "add", "-f", "upstream", upstreamURL)
cloneCmd.Stdout = os.Stdout
cloneCmd.Stderr = os.Stderr
return run.PrepareCmd(cloneCmd).Run()
}
func repoCreate(cmd *cobra.Command, args []string) error {
projectDir, projectDirErr := git.ToplevelDir()
@ -369,10 +240,6 @@ func repoCreate(cmd *cobra.Command, args []string) error {
return nil
}
func isURL(arg string) bool {
return strings.HasPrefix(arg, "http:/") || strings.HasPrefix(arg, "https:/")
}
var Since = func(t time.Time) time.Duration {
return time.Since(t)
}
@ -406,7 +273,7 @@ func repoFork(cmd *cobra.Command, args []string) error {
} else {
repoArg := args[0]
if isURL(repoArg) {
if utils.IsURL(repoArg) {
parsedURL, err := url.Parse(repoArg)
if err != nil {
return fmt.Errorf("did not understand argument: %w", err)
@ -549,12 +416,24 @@ func repoFork(cmd *cobra.Command, args []string) error {
}
if cloneDesired {
forkedRepoCloneURL := formatRemoteURL(cmd, forkedRepo)
cloneDir, err := runClone(forkedRepoCloneURL, []string{})
cloneDir, err := git.RunClone(forkedRepoCloneURL, []string{})
if err != nil {
return fmt.Errorf("failed to clone fork: %w", err)
}
err = addUpstreamRemote(cmd, repoToFork, cloneDir)
// TODO This is overly wordy and I'd like to streamline this.
cfg, err := ctx.Config()
if err != nil {
return err
}
protocol, err := cfg.Get("", "git_protocol")
if err != nil {
return err
}
upstreamURL := ghrepo.FormatRemoteURL(repoToFork, protocol)
err = git.AddUpstreamRemote(upstreamURL, cloneDir)
if err != nil {
return err
}
@ -576,145 +455,6 @@ var Confirm = func(prompt string, result *bool) error {
return survey.AskOne(p, result)
}
func repoView(cmd *cobra.Command, args []string) error {
ctx := contextForCommand(cmd)
apiClient, err := apiClientForContext(ctx)
if err != nil {
return err
}
var toView ghrepo.Interface
if len(args) == 0 {
var err error
toView, err = determineBaseRepo(apiClient, cmd, ctx)
if err != nil {
return err
}
} else {
repoArg := args[0]
if isURL(repoArg) {
parsedURL, err := url.Parse(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(repoArg)
if err != nil {
return fmt.Errorf("argument error: %w", err)
}
}
}
repo, err := api.GitHubRepo(apiClient, toView)
if err != nil {
return err
}
web, err := cmd.Flags().GetBool("web")
if err != nil {
return err
}
openURL := generateRepoURL(toView, "")
if web {
if connectedToTerminal(cmd) {
fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", displayURL(openURL))
}
return utils.OpenInBrowser(openURL)
}
fullName := ghrepo.FullName(toView)
if !connectedToTerminal(cmd) {
readme, err := api.RepositoryReadme(apiClient, toView)
if err != nil {
return err
}
out := cmd.OutOrStdout()
fmt.Fprintf(out, "name:\t%s\n", fullName)
fmt.Fprintf(out, "description:\t%s\n", repo.Description)
fmt.Fprintln(out, "--")
fmt.Fprintf(out, readme.Content)
return nil
}
repoTmpl := `
{{.FullName}}
{{.Description}}
{{.Readme}}
{{.View}}
`
tmpl, err := template.New("repo").Parse(repoTmpl)
if err != nil {
return err
}
readme, err := api.RepositoryReadme(apiClient, toView)
var notFound *api.NotFoundError
if err != nil && !errors.As(err, &notFound) {
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)),
}
out := colorableOut(cmd)
err = tmpl.Execute(out, repoData)
if err != nil {
return err
}
return nil
}
func repoCredits(cmd *cobra.Command, args []string) error {
return credits(cmd, args)
}
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

@ -4,7 +4,6 @@ import (
"encoding/json"
"io/ioutil"
"os/exec"
"reflect"
"regexp"
"strings"
"testing"
@ -437,190 +436,6 @@ func TestRepoFork_in_parent_survey_no(t *testing.T) {
}
}
func TestParseExtraArgs(t *testing.T) {
type Wanted struct {
args []string
dir string
}
tests := []struct {
name string
args []string
want Wanted
}{
{
name: "args and target",
args: []string{"target_directory", "-o", "upstream", "--depth", "1"},
want: Wanted{
args: []string{"-o", "upstream", "--depth", "1"},
dir: "target_directory",
},
},
{
name: "only args",
args: []string{"-o", "upstream", "--depth", "1"},
want: Wanted{
args: []string{"-o", "upstream", "--depth", "1"},
dir: "",
},
},
{
name: "only target",
args: []string{"target_directory"},
want: Wanted{
args: []string{},
dir: "target_directory",
},
},
{
name: "no args",
args: []string{},
want: Wanted{
args: []string{},
dir: "",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
args, dir := parseCloneArgs(tt.args)
got := Wanted{
args: args,
dir: dir,
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("got %#v want %#v", got, tt.want)
}
})
}
}
func TestRepoClone(t *testing.T) {
tests := []struct {
name string
args string
want string
}{
{
name: "shorthand",
args: "repo clone OWNER/REPO",
want: "git clone https://github.com/OWNER/REPO.git",
},
{
name: "shorthand with directory",
args: "repo clone OWNER/REPO target_directory",
want: "git clone https://github.com/OWNER/REPO.git target_directory",
},
{
name: "clone arguments",
args: "repo clone 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: "repo clone 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: "repo clone https://github.com/OWNER/REPO",
want: "git clone https://github.com/OWNER/REPO",
},
{
name: "SSH URL",
args: "repo clone 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) {
http := initFakeHTTP()
http.Register(
httpmock.GraphQL(`query RepositoryFindParent\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"parent": null
} } }
`))
cs, restore := test.InitCmdStubber()
defer restore()
cs.Stub("") // git clone
output, err := RunCommand(tt.args)
if err != nil {
t.Fatalf("error running command `repo clone`: %v", err)
}
eq(t, output.String(), "")
eq(t, output.Stderr(), "")
eq(t, cs.Count, 1)
eq(t, strings.Join(cs.Calls[0].Args, " "), tt.want)
})
}
}
func TestRepoClone_hasParent(t *testing.T) {
http := initFakeHTTP()
http.Register(
httpmock.GraphQL(`query RepositoryFindParent\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"parent": {
"owner": {"login": "hubot"},
"name": "ORIG"
}
} } }
`))
cs, restore := test.InitCmdStubber()
defer restore()
cs.Stub("") // git clone
cs.Stub("") // git remote add
_, err := RunCommand("repo clone OWNER/REPO")
if err != nil {
t.Fatalf("error running command `repo clone`: %v", err)
}
eq(t, cs.Count, 2)
eq(t, strings.Join(cs.Calls[1].Args, " "), "git -C REPO remote add -f upstream https://github.com/hubot/ORIG.git")
}
func TestRepo_withoutUsername(t *testing.T) {
http := initFakeHTTP()
http.Register(
httpmock.GraphQL(`query UserCurrent\b`),
httpmock.StringResponse(`
{ "data": { "viewer": {
"login": "OWNER"
}}}`))
http.Register(
httpmock.GraphQL(`query RepositoryFindParent\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"parent": null
} } }`))
cs, restore := test.InitCmdStubber()
defer restore()
cs.Stub("") // git clone
output, err := RunCommand("repo clone REPO")
if err != nil {
t.Fatalf("error running command `repo clone`: %v", err)
}
eq(t, output.String(), "")
eq(t, output.Stderr(), "")
eq(t, cs.Count, 1)
eq(t, strings.Join(cs.Calls[0].Args, " "), "git clone https://github.com/OWNER/REPO.git")
}
func TestRepoCreate(t *testing.T) {
ctx := context.NewBlank()
ctx.SetBranch("master")
@ -827,221 +642,3 @@ func TestRepoCreate_orgWithTeam(t *testing.T) {
t.Errorf("expected %q, got %q", "TEAMID", teamID)
}
}
func TestRepoView_web(t *testing.T) {
initBlankContext("", "OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{ }`))
var seenCmd *exec.Cmd
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
seenCmd = cmd
return &test.OutputStub{}
})
defer restoreCmd()
defer stubTerminal(true)()
output, err := RunCommand("repo view -w")
if err != nil {
t.Errorf("error running command `repo view`: %v", err)
}
eq(t, output.String(), "")
eq(t, output.Stderr(), "Opening github.com/OWNER/REPO in your browser.\n")
if seenCmd == nil {
t.Fatal("expected a command to run")
}
url := seenCmd.Args[len(seenCmd.Args)-1]
eq(t, url, "https://github.com/OWNER/REPO")
}
func TestRepoView_web_ownerRepo(t *testing.T) {
ctx := context.NewBlank()
ctx.SetBranch("master")
initContext = func() context.Context {
return ctx
}
http := initFakeHTTP()
http.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{ }`))
var seenCmd *exec.Cmd
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
seenCmd = cmd
return &test.OutputStub{}
})
defer restoreCmd()
defer stubTerminal(true)()
output, err := RunCommand("repo view -w cli/cli")
if err != nil {
t.Errorf("error running command `repo view`: %v", err)
}
eq(t, output.String(), "")
eq(t, output.Stderr(), "Opening github.com/cli/cli in your browser.\n")
if seenCmd == nil {
t.Fatal("expected a command to run")
}
url := seenCmd.Args[len(seenCmd.Args)-1]
eq(t, url, "https://github.com/cli/cli")
}
func TestRepoView_web_fullURL(t *testing.T) {
ctx := context.NewBlank()
ctx.SetBranch("master")
initContext = func() context.Context {
return ctx
}
http := initFakeHTTP()
http.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{ }`))
var seenCmd *exec.Cmd
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
seenCmd = cmd
return &test.OutputStub{}
})
defer restoreCmd()
defer stubTerminal(true)()
output, err := RunCommand("repo view -w https://github.com/cli/cli")
if err != nil {
t.Errorf("error running command `repo view`: %v", err)
}
eq(t, output.String(), "")
eq(t, output.Stderr(), "Opening github.com/cli/cli in your browser.\n")
if seenCmd == nil {
t.Fatal("expected a command to run")
}
url := seenCmd.Args[len(seenCmd.Args)-1]
eq(t, url, "https://github.com/cli/cli")
}
func TestRepoView(t *testing.T) {
initBlankContext("", "OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": {
"repository": {
"description": "social distancing"
} } }`))
http.Register(
httpmock.REST("GET", "repos/OWNER/REPO/readme"),
httpmock.StringResponse(`
{ "name": "readme.md",
"content": "IyB0cnVseSBjb29sIHJlYWRtZSBjaGVjayBpdCBvdXQ="}`))
defer stubTerminal(true)()
output, err := RunCommand("repo view")
if err != nil {
t.Errorf("error running command `repo view`: %v", err)
}
test.ExpectLines(t, output.String(),
"OWNER/REPO",
"social distancing",
"truly cool readme",
"View this repository on GitHub: https://github.com/OWNER/REPO")
}
func TestRepoView_nonmarkdown_readme(t *testing.T) {
initBlankContext("", "OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": {
"repository": {
"description": "social distancing"
} } }`))
http.Register(
httpmock.REST("GET", "repos/OWNER/REPO/readme"),
httpmock.StringResponse(`
{ "name": "readme.org",
"content": "IyB0cnVseSBjb29sIHJlYWRtZSBjaGVjayBpdCBvdXQ="}`))
defer stubTerminal(true)()
output, err := RunCommand("repo view")
if err != nil {
t.Errorf("error running command `repo view`: %v", err)
}
test.ExpectLines(t, output.String(),
"OWNER/REPO",
"social distancing",
"# truly cool readme",
"View this repository on GitHub: https://github.com/OWNER/REPO")
}
func TestRepoView_blanks(t *testing.T) {
initBlankContext("", "OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.Register(httpmock.GraphQL(`query RepositoryInfo\b`), httpmock.StringResponse("{}"))
http.Register(
httpmock.REST("GET", "repos/OWNER/REPO/readme"),
httpmock.StatusStringResponse(404, `{}`))
defer stubTerminal(true)()
output, err := RunCommand("repo view")
if err != nil {
t.Errorf("error running command `repo view`: %v", err)
}
test.ExpectLines(t, output.String(),
"OWNER/REPO",
"No description provided",
"This repository does not have a README",
"View this repository on GitHub: https://github.com/OWNER/REPO")
}
func TestRepoView_nontty(t *testing.T) {
initBlankContext("", "OWNER/REPO", "master")
http := initFakeHTTP()
http.StubRepoResponse("OWNER", "REPO")
http.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{ "data": {
"repository": {
"description": "social distancing"
} } }`))
http.Register(
httpmock.REST("GET", "repos/OWNER/REPO/readme"),
httpmock.StringResponse(`
{ "name": "readme.md",
"content": "IyB0cnVseSBjb29sIHJlYWRtZSBjaGVjayBpdCBvdXQ="}`))
defer stubTerminal(false)()
output, err := RunCommand("repo view")
if err != nil {
t.Errorf("error running command `repo view`: %v", err)
}
test.ExpectLines(t, output.String(),
"OWNER/REPO",
"social distancing",
"# truly cool readme check it out",
)
}

View file

@ -21,6 +21,8 @@ import (
"github.com/cli/cli/internal/run"
apiCmd "github.com/cli/cli/pkg/cmd/api"
gistCreateCmd "github.com/cli/cli/pkg/cmd/gist/create"
repoCloneCmd "github.com/cli/cli/pkg/cmd/repo/clone"
repoViewCmd "github.com/cli/cli/pkg/cmd/repo/view"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/iostreams"
"github.com/cli/cli/utils"
@ -94,6 +96,15 @@ func init() {
ctx := context.New()
return ctx.BaseRepo()
},
Config: func() (config.Config, error) {
cfg, err := config.ParseDefaultConfig()
if errors.Is(err, os.ErrNotExist) {
cfg = config.NewBlankConfig()
} else if err != nil {
return nil, err
}
return cfg, nil
},
}
RootCmd.AddCommand(apiCmd.NewCmdApi(cmdFactory, nil))
@ -104,6 +115,39 @@ func init() {
}
RootCmd.AddCommand(gistCmd)
gistCmd.AddCommand(gistCreateCmd.NewCmdCreate(cmdFactory, nil))
resolvedBaseRepo := func() (ghrepo.Interface, error) {
httpClient, err := cmdFactory.HttpClient()
if err != nil {
return nil, err
}
apiClient := api.NewClientFromHTTP(httpClient)
ctx := context.New()
remotes, err := ctx.Remotes()
if err != nil {
return nil, err
}
repoContext, err := context.ResolveRemotesToRepos(remotes, apiClient, "")
if err != nil {
return nil, err
}
baseRepo, err := repoContext.BaseRepo()
if err != nil {
return nil, err
}
return baseRepo, nil
}
repoResolvingCmdFactory := *cmdFactory
repoResolvingCmdFactory.BaseRepo = resolvedBaseRepo
RootCmd.AddCommand(repoCmd)
repoCmd.AddCommand(repoViewCmd.NewCmdView(&repoResolvingCmdFactory, nil))
repoCmd.AddCommand(repoCloneCmd.NewCmdClone(cmdFactory, nil))
}
// RootCmd is the entry point of command-line execution
@ -322,6 +366,7 @@ func determineBaseRepo(apiClient *api.Client, cmd *cobra.Command, ctx context.Co
return baseRepo, nil
}
// TODO there is a parallel implementation for isolated commands
func formatRemoteURL(cmd *cobra.Command, repo ghrepo.Interface) string {
ctx := contextForCommand(cmd)

View file

@ -7,6 +7,7 @@ import (
"net/url"
"os"
"os/exec"
"path"
"regexp"
"strings"
@ -224,6 +225,48 @@ func CheckoutBranch(branch string) error {
return err
}
func parseCloneArgs(extraArgs []string) (args []string, target string) {
args = extraArgs
if len(args) > 0 {
if !strings.HasPrefix(args[0], "-") {
target, args = args[0], args[1:]
}
}
return
}
func RunClone(cloneURL string, args []string) (target string, err error) {
cloneArgs, target := parseCloneArgs(args)
cloneArgs = append(cloneArgs, cloneURL)
// If the args contain an explicit target, pass it to clone
// otherwise, parse the URL to determine where git cloned it to so we can return it
if target != "" {
cloneArgs = append(cloneArgs, target)
} else {
target = path.Base(strings.TrimSuffix(cloneURL, ".git"))
}
cloneArgs = append([]string{"clone"}, cloneArgs...)
cloneCmd := GitCommand(cloneArgs...)
cloneCmd.Stdin = os.Stdin
cloneCmd.Stdout = os.Stdout
cloneCmd.Stderr = os.Stderr
err = run.PrepareCmd(cloneCmd).Run()
return
}
func AddUpstreamRemote(upstreamURL, cloneDir string) error {
cloneCmd := GitCommand("-C", cloneDir, "remote", "add", "-f", "upstream", upstreamURL)
cloneCmd.Stdout = os.Stdout
cloneCmd.Stderr = os.Stderr
return run.PrepareCmd(cloneCmd).Run()
}
func isFilesystemPath(p string) bool {
return p == "." || strings.HasPrefix(p, "./") || strings.HasPrefix(p, "/")
}

View file

@ -2,6 +2,7 @@ package git
import (
"os/exec"
"reflect"
"testing"
"github.com/cli/cli/internal/run"
@ -94,3 +95,62 @@ func Test_CurrentBranch_unexpected_error(t *testing.T) {
t.Errorf("expected 1 git call, saw %d", len(cs.Calls))
}
}
func TestParseExtraCloneArgs(t *testing.T) {
type Wanted struct {
args []string
dir string
}
tests := []struct {
name string
args []string
want Wanted
}{
{
name: "args and target",
args: []string{"target_directory", "-o", "upstream", "--depth", "1"},
want: Wanted{
args: []string{"-o", "upstream", "--depth", "1"},
dir: "target_directory",
},
},
{
name: "only args",
args: []string{"-o", "upstream", "--depth", "1"},
want: Wanted{
args: []string{"-o", "upstream", "--depth", "1"},
dir: "",
},
},
{
name: "only target",
args: []string{"target_directory"},
want: Wanted{
args: []string{},
dir: "target_directory",
},
},
{
name: "no args",
args: []string{},
want: Wanted{
args: []string{},
dir: "",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
args, dir := parseCloneArgs(tt.args)
got := Wanted{
args: args,
dir: dir,
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("got %#v want %#v", got, tt.want)
}
})
}
}

View file

@ -77,6 +77,23 @@ func IsSame(a, b Interface) bool {
normalizeHostname(a.RepoHost()) == normalizeHostname(b.RepoHost())
}
func GenerateRepoURL(repo Interface, p string, args ...interface{}) string {
baseURL := fmt.Sprintf("https://%s/%s/%s", repo.RepoHost(), repo.RepoOwner(), repo.RepoName())
if p != "" {
return baseURL + "/" + fmt.Sprintf(p, args...)
}
return baseURL
}
// TODO there is a parallel implementation for non-isolated commands
func FormatRemoteURL(repo Interface, protocol string) string {
if protocol == "ssh" {
return fmt.Sprintf("git@%s:%s/%s.git", repo.RepoHost(), repo.RepoOwner(), repo.RepoName())
}
return fmt.Sprintf("https://%s/%s/%s.git", repo.RepoHost(), repo.RepoOwner(), repo.RepoName())
}
type ghRepo struct {
owner string
name string

View file

@ -70,8 +70,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 {

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

@ -0,0 +1,126 @@
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/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, "/") {
currentUser, err := api.CurrentLoginName(apiClient)
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("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

View file

@ -3,6 +3,7 @@ package utils
import (
"fmt"
"io"
"net/url"
"strings"
"time"
@ -102,3 +103,15 @@ var StopSpinner = func(s *spinner.Spinner) {
func Spinner(w io.Writer) *spinner.Spinner {
return spinner.New(spinner.CharSets[11], 400*time.Millisecond, spinner.WithWriter(w))
}
func IsURL(s string) bool {
return strings.HasPrefix(s, "http:/") || strings.HasPrefix(s, "https:/")
}
func DisplayURL(urlStr string) string {
u, err := url.Parse(urlStr)
if err != nil {
return urlStr
}
return u.Hostname() + u.Path
}