github.com/abdfnx/gh-api@v0.0.0-20210414084727-f5432eec23b8/pkg/cmd/repo/clone/clone.go (about)

     1  package clone
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"strings"
     7  
     8  	"github.com/MakeNowJust/heredoc"
     9  	"github.com/abdfnx/gh-api/api"
    10  	"github.com/abdfnx/gh-api/git"
    11  	"github.com/abdfnx/gh-api/internal/config"
    12  	"github.com/abdfnx/gh-api/internal/ghrepo"
    13  	"github.com/abdfnx/gh-api/pkg/cmdutil"
    14  	"github.com/abdfnx/gh-api/pkg/iostreams"
    15  	"github.com/spf13/cobra"
    16  	"github.com/spf13/pflag"
    17  )
    18  
    19  type CloneOptions struct {
    20  	HttpClient func() (*http.Client, error)
    21  	Config     func() (config.Config, error)
    22  	IO         *iostreams.IOStreams
    23  
    24  	GitArgs    []string
    25  	Repository string
    26  }
    27  
    28  func NewCmdClone(f *cmdutil.Factory, runF func(*CloneOptions) error) *cobra.Command {
    29  	opts := &CloneOptions{
    30  		IO:         f.IOStreams,
    31  		HttpClient: f.HttpClient,
    32  		Config:     f.Config,
    33  	}
    34  
    35  	cmd := &cobra.Command{
    36  		DisableFlagsInUseLine: true,
    37  
    38  		Use:   "clone <repository> [<directory>] [-- <gitflags>...]",
    39  		Args:  cmdutil.MinimumArgs(1, "cannot clone: repository argument required"),
    40  		Short: "Clone a repository locally",
    41  		Long: heredoc.Doc(`
    42  			Clone a GitHub repository locally.
    43  
    44  			If the "OWNER/" portion of the "OWNER/REPO" repository argument is omitted, it
    45  			defaults to the name of the authenticating user.
    46  
    47  			Pass additional 'git clone' flags by listing them after '--'.
    48  		`),
    49  		RunE: func(cmd *cobra.Command, args []string) error {
    50  			opts.Repository = args[0]
    51  			opts.GitArgs = args[1:]
    52  
    53  			if runF != nil {
    54  				return runF(opts)
    55  			}
    56  
    57  			return cloneRun(opts)
    58  		},
    59  	}
    60  
    61  	cmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
    62  		if err == pflag.ErrHelp {
    63  			return err
    64  		}
    65  		return &cmdutil.FlagError{Err: fmt.Errorf("%w\nSeparate git clone flags with '--'.", err)}
    66  	})
    67  
    68  	return cmd
    69  }
    70  
    71  func cloneRun(opts *CloneOptions) error {
    72  	httpClient, err := opts.HttpClient()
    73  	if err != nil {
    74  		return err
    75  	}
    76  
    77  	cfg, err := opts.Config()
    78  	if err != nil {
    79  		return err
    80  	}
    81  
    82  	apiClient := api.NewClientFromHTTP(httpClient)
    83  
    84  	repositoryIsURL := strings.Contains(opts.Repository, ":")
    85  	repositoryIsFullName := !repositoryIsURL && strings.Contains(opts.Repository, "/")
    86  
    87  	var repo ghrepo.Interface
    88  	var protocol string
    89  
    90  	if repositoryIsURL {
    91  		repoURL, err := git.ParseURL(opts.Repository)
    92  		if err != nil {
    93  			return err
    94  		}
    95  		repo, err = ghrepo.FromURL(repoURL)
    96  		if err != nil {
    97  			return err
    98  		}
    99  		if repoURL.Scheme == "git+ssh" {
   100  			repoURL.Scheme = "ssh"
   101  		}
   102  		protocol = repoURL.Scheme
   103  	} else {
   104  		var fullName string
   105  		if repositoryIsFullName {
   106  			fullName = opts.Repository
   107  		} else {
   108  			host, err := cfg.DefaultHost()
   109  			if err != nil {
   110  				return err
   111  			}
   112  			currentUser, err := api.CurrentLoginName(apiClient, host)
   113  			if err != nil {
   114  				return err
   115  			}
   116  			fullName = currentUser + "/" + opts.Repository
   117  		}
   118  
   119  		repo, err = ghrepo.FromFullName(fullName)
   120  		if err != nil {
   121  			return err
   122  		}
   123  
   124  		protocol, err = cfg.Get(repo.RepoHost(), "git_protocol")
   125  		if err != nil {
   126  			return err
   127  		}
   128  	}
   129  
   130  	wantsWiki := strings.HasSuffix(repo.RepoName(), ".wiki")
   131  	if wantsWiki {
   132  		repoName := strings.TrimSuffix(repo.RepoName(), ".wiki")
   133  		repo = ghrepo.NewWithHost(repo.RepoOwner(), repoName, repo.RepoHost())
   134  	}
   135  
   136  	// Load the repo from the API to get the username/repo name in its
   137  	// canonical capitalization
   138  	canonicalRepo, err := api.GitHubRepo(apiClient, repo)
   139  	if err != nil {
   140  		return err
   141  	}
   142  	canonicalCloneURL := ghrepo.FormatRemoteURL(canonicalRepo, protocol)
   143  
   144  	// If repo HasWikiEnabled and wantsWiki is true then create a new clone URL
   145  	if wantsWiki {
   146  		if !canonicalRepo.HasWikiEnabled {
   147  			return fmt.Errorf("The '%s' repository does not have a wiki", ghrepo.FullName(canonicalRepo))
   148  		}
   149  		canonicalCloneURL = strings.TrimSuffix(canonicalCloneURL, ".git") + ".wiki.git"
   150  	}
   151  
   152  	cloneDir, err := git.RunClone(canonicalCloneURL, opts.GitArgs)
   153  	if err != nil {
   154  		return err
   155  	}
   156  
   157  	// If the repo is a fork, add the parent as an upstream
   158  	if canonicalRepo.Parent != nil {
   159  		protocol, err := cfg.Get(canonicalRepo.Parent.RepoHost(), "git_protocol")
   160  		if err != nil {
   161  			return err
   162  		}
   163  		upstreamURL := ghrepo.FormatRemoteURL(canonicalRepo.Parent, protocol)
   164  
   165  		err = git.AddUpstreamRemote(upstreamURL, cloneDir, []string{canonicalRepo.Parent.DefaultBranchRef.Name})
   166  		if err != nil {
   167  			return err
   168  		}
   169  	}
   170  
   171  	return nil
   172  }