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  }