github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/repo/clone/clone.go (about)

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