github.com/Benchkram/bob@v0.0.0-20220321080157-7c8f3876e225/bob/clone.go (about) 1 package bob 2 3 import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "os" 8 "path/filepath" 9 "strings" 10 11 "github.com/Benchkram/bob/pkg/cmdutil" 12 "github.com/Benchkram/bob/pkg/file" 13 "github.com/Benchkram/bob/pkg/usererror" 14 "github.com/Benchkram/errz" 15 16 "github.com/logrusorgru/aurora" 17 ) 18 19 var ErrNoValidURLToClone = fmt.Errorf("No valid URL to clone found.") 20 21 // cloneURLItem used to map URL item from the `makeURLPriorityList` 22 // with it's protocol for logging purpuse 23 type cloneURLItem struct { 24 url string 25 protocol string 26 } 27 28 // Clone repos which are not yet in the workspace. 29 // Uses priority urls ssh >> https >> file. 30 // 31 // failFast will not prompt the user in case of an error. 32 // 33 // TODO: it still happens that git prompts for user input 34 // in case of a missing password on https. 35 func (b *B) Clone(failFast bool) (err error) { 36 defer errz.Recover(&err) 37 38 for _, repo := range b.Repositories { 39 40 prioritylist, err := makeURLPriorityList(repo) 41 errz.Fatal(err) 42 43 // Check if repository is already checked out. 44 if file.Exists(repo.Name) { 45 fmt.Printf("%s\n", aurora.Yellow(fmt.Sprintf("Skipping %s as the directory `%s` already exists", repo.Name, repo.Name))) 46 continue 47 } 48 49 // returns user error if no possible url found 50 if len(prioritylist) == 0 { 51 return usererror.Wrapm(ErrNoValidURLToClone, "Failed to clone repository") 52 } 53 54 var out []byte 55 // Starts cloning from the first item of the priority list, 56 // break for successfull cloning and fallback to next item in 57 // the map in case of failure 58 for i, item := range prioritylist { 59 out, err = cmdutil.RunGitWithOutput(b.dir, "clone", item.url, "--progress") 60 if err == nil { 61 break 62 } 63 64 fmt.Println(err.Error()) 65 err = nil 66 67 // fail early, useful when used on ci. 68 if failFast { 69 return usererror.Wrap(fmt.Errorf("abort")) 70 } 71 72 // Get user feedback in case of a failure before trying the 73 // next clone method. 74 fmt.Printf("%s\n", aurora.Yellow(fmt.Sprintf("Failed to clone %s using %s", repo.Name, item.protocol))) 75 if i < len(prioritylist)-1 { 76 77 wd, _ := os.Getwd() 78 target := filepath.Join(b.dir, repo.Name) 79 target, _ = filepath.Rel(wd, target) 80 81 fmt.Printf("[%s] is likely in an invalid state. Want to delete [%s] and clone using [%s]: (y/(a)bort/(i)ignore) ", 82 aurora.Bold(target), 83 aurora.Bold(target), 84 aurora.Bold(prioritylist[i+1].protocol), 85 ) 86 reader := bufio.NewReader(os.Stdin) 87 text, _ := reader.ReadString('\n') 88 text = strings.Replace(text, "\n", "", -1) 89 text = strings.ToLower(text) 90 91 if text == "y" { 92 fmt.Println() 93 if file.Exists(target) { 94 _ = os.RemoveAll(target) 95 } 96 } else if text == "i" { 97 fmt.Printf("ignoring %s\n\n", target) 98 // Clear output as it's already been printed. 99 out = []byte{} 100 break 101 } else { 102 return usererror.Wrap(fmt.Errorf("abort")) 103 } 104 } 105 } 106 107 if len(out) > 0 { 108 buf := FprintCloneOutput(repo.Name, out, err == nil) 109 fmt.Println(buf.String()) 110 } 111 112 err = b.gitignoreAdd(repo.Name) 113 errz.Fatal(err) 114 } 115 116 return b.write() 117 } 118 119 // CloneRepo repo and sub repositories recursively. 120 // failFast will not prompt the user in case of an error. 121 func (b *B) CloneRepo(repoURL string, failFast bool) (_ string, err error) { 122 defer errz.Recover(&err) 123 124 out, err := cmdutil.RunGitWithOutput(b.dir, "clone", repoURL, "--progress") 125 errz.Fatal(err) 126 127 buf := FprintCloneOutput(".", out, err == nil) 128 fmt.Println(buf.String()) 129 130 repo, err := Parse(repoURL) 131 errz.Fatal(err) 132 133 absRepoPath, err := filepath.Abs(repo.Name()) 134 errz.Fatal(err) 135 136 wd, err := os.Getwd() 137 errz.Fatal(err) 138 139 // change currenct directory to inside the repository 140 err = os.Chdir(absRepoPath) 141 errz.Fatal(err) 142 143 // change revert back to current working directory 144 defer func() { _ = os.Chdir(wd) }() 145 146 bob, err := Bob( 147 WithDir(absRepoPath), 148 WithRequireBobConfig(), 149 ) 150 errz.Fatal(err) 151 152 if err := bob.Clone(failFast); err != nil { 153 return "", err 154 } 155 156 return repo.Name(), nil 157 } 158 159 // makeURLPriorityList returns list of cloneURLItem from forwarded repo, 160 // ordered by the priority type, ssh >> https >> file. 161 // 162 // It ignores ssh/http if any of them set to "" 163 // 164 // It als checks if it is a valid git repo, 165 // as someone might changed it on disk. 166 func makeURLPriorityList(repo Repo) ([]cloneURLItem, error) { 167 168 var urls []cloneURLItem 169 170 if repo.SSHUrl != "" { 171 repoFromSSH, err := Parse(repo.SSHUrl) 172 if err != nil { 173 return nil, err 174 } 175 urls = append(urls, cloneURLItem{ 176 url: repoFromSSH.SSH.String(), 177 protocol: "ssh", 178 }) 179 } 180 181 if repo.HTTPSUrl != "" { 182 repoFromHTTPS, err := Parse(repo.HTTPSUrl) 183 if err != nil { 184 return nil, err 185 } 186 urls = append(urls, cloneURLItem{ 187 url: repoFromHTTPS.HTTPS.String(), 188 protocol: "https", 189 }) 190 } 191 192 if repo.LocalUrl != "" { 193 urls = append(urls, cloneURLItem{ 194 url: repo.LocalUrl, 195 protocol: "local", 196 }) 197 } 198 199 return urls, nil 200 } 201 202 // FprintCloneOutput returns formatted output buffer with repository title 203 // from git clone output. 204 func FprintCloneOutput(reponame string, output []byte, success bool) *bytes.Buffer { 205 buf := FprintRepoTitle(reponame, 20, success) 206 if len(output) > 0 { 207 for _, line := range ConvertToLines(output) { 208 modified := fmt.Sprint(aurora.Gray(12, line)) 209 if !success { 210 modified = fmt.Sprint(aurora.Red(line)) 211 } 212 fmt.Fprintln(buf, modified) 213 } 214 } 215 216 return buf 217 } 218 219 // FprintRepoTitle returns repo title buffer with success/error label 220 func FprintRepoTitle(reponame string, maxlen int, success bool) *bytes.Buffer { 221 buf := bytes.NewBuffer(nil) 222 spacing := "%-" + fmt.Sprint(maxlen) + "s" 223 repopath := fmt.Sprintf(spacing, sanitizeReponame(reponame)) 224 title := fmt.Sprint(repopath, "\t", aurora.Green("success")) 225 if !success { 226 title = fmt.Sprint(repopath, "\t", aurora.Red("error")) 227 } 228 fmt.Fprint(buf, title) 229 fmt.Fprintln(buf) 230 231 return buf 232 } 233 234 // sanitizeReponame returns sanitized reponame. 235 // 236 // Example: "." => "/", "second-level" => "second-level/" 237 func sanitizeReponame(reponame string) string { 238 repopath := reponame 239 if reponame == "." { 240 repopath = "/" 241 } else if repopath[len(repopath)-1:] != "/" { 242 repopath = repopath + "/" 243 } 244 return repopath 245 } 246 247 // ConvertToLines converts bytes into a list of strings separeted by newline 248 func ConvertToLines(output []byte) []string { 249 lines := strings.TrimSuffix(string(output), "\n") 250 return strings.Split(lines, "\n") 251 }