Merge pull request #1416 from vilmibm/migrate-repo-clone
isolate repo clone
This commit is contained in:
commit
75120344c3
19 changed files with 1249 additions and 740 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
290
command/repo.go
290
command/repo.go
|
|
@ -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, ¬Found) {
|
||||
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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
43
git/git.go
43
git/git.go
|
|
@ -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, "/")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
126
pkg/cmd/repo/clone/clone.go
Normal 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
|
||||
}
|
||||
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("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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue