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 }