github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/internal/packager/git/clone.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // SPDX-FileCopyrightText: 2021-Present The Jackal Authors
     3  
     4  // Package git contains functions for interacting with git repositories.
     5  package git
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"strings"
    11  
    12  	"github.com/Racer159/jackal/src/pkg/message"
    13  	"github.com/Racer159/jackal/src/pkg/utils"
    14  	"github.com/Racer159/jackal/src/pkg/utils/exec"
    15  	"github.com/go-git/go-git/v5"
    16  	goConfig "github.com/go-git/go-git/v5/config"
    17  	"github.com/go-git/go-git/v5/plumbing"
    18  )
    19  
    20  // clone performs a `git clone` of a given repo.
    21  func (g *Git) clone(gitURL string, ref plumbing.ReferenceName, shallow bool) error {
    22  	cloneOptions := &git.CloneOptions{
    23  		URL:        gitURL,
    24  		Progress:   g.Spinner,
    25  		RemoteName: onlineRemoteName,
    26  	}
    27  
    28  	// Don't clone all tags / refs if we're cloning a specific tag or branch.
    29  	if ref.IsTag() || ref.IsBranch() {
    30  		cloneOptions.Tags = git.NoTags
    31  		cloneOptions.ReferenceName = ref
    32  		cloneOptions.SingleBranch = true
    33  	}
    34  
    35  	// If this is a shallow clone set the depth to 1
    36  	if shallow {
    37  		cloneOptions.Depth = 1
    38  	}
    39  
    40  	// Setup git credentials if we have them, ignore if we don't.
    41  	gitCred := utils.FindAuthForHost(gitURL)
    42  	if gitCred != nil {
    43  		cloneOptions.Auth = &gitCred.Auth
    44  	}
    45  
    46  	// Clone the given repo.
    47  	repo, err := git.PlainClone(g.GitPath, false, cloneOptions)
    48  	if err != nil {
    49  		message.Notef("Falling back to host 'git', failed to clone the repo %q with Jackal: %s", gitURL, err.Error())
    50  		return g.gitCloneFallback(gitURL, ref, shallow)
    51  	}
    52  
    53  	// If we're cloning the whole repo, we need to also fetch the other branches besides the default.
    54  	if ref == emptyRef {
    55  		fetchOpts := &git.FetchOptions{
    56  			RemoteName: onlineRemoteName,
    57  			Progress:   g.Spinner,
    58  			RefSpecs:   []goConfig.RefSpec{"refs/*:refs/*"},
    59  			Tags:       git.AllTags,
    60  		}
    61  
    62  		if gitCred != nil {
    63  			fetchOpts.Auth = &gitCred.Auth
    64  		}
    65  
    66  		if err := repo.Fetch(fetchOpts); err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
    67  			return err
    68  		}
    69  	}
    70  
    71  	return nil
    72  }
    73  
    74  // gitCloneFallback is a fallback if go-git fails to clone a repo.
    75  func (g *Git) gitCloneFallback(gitURL string, ref plumbing.ReferenceName, shallow bool) error {
    76  	// If we can't clone with go-git, fallback to the host clone
    77  	// Only support "all tags" due to the azure clone url format including a username
    78  	cloneArgs := []string{"clone", "--origin", onlineRemoteName, gitURL, g.GitPath}
    79  
    80  	// Don't clone all tags / refs if we're cloning a specific tag or branch.
    81  	if ref.IsTag() || ref.IsBranch() {
    82  		cloneArgs = append(cloneArgs, "--no-tags")
    83  		cloneArgs = append(cloneArgs, "-b", ref.Short())
    84  		cloneArgs = append(cloneArgs, "--single-branch")
    85  	}
    86  
    87  	// If this is a shallow clone set the depth to 1
    88  	if shallow {
    89  		cloneArgs = append(cloneArgs, "--depth", "1")
    90  	}
    91  
    92  	cloneExecConfig := exec.Config{
    93  		Stdout: g.Spinner,
    94  		Stderr: g.Spinner,
    95  	}
    96  
    97  	message.Command("git %s", strings.Join(cloneArgs, " "))
    98  
    99  	_, _, err := exec.CmdWithContext(context.TODO(), cloneExecConfig, "git", cloneArgs...)
   100  	if err != nil {
   101  		return err
   102  	}
   103  
   104  	// If we're cloning the whole repo, we need to also fetch the other branches besides the default.
   105  	if ref == emptyRef {
   106  		fetchArgs := []string{"fetch", "--tags", "--update-head-ok", onlineRemoteName, "refs/*:refs/*"}
   107  
   108  		fetchExecConfig := exec.Config{
   109  			Stdout: g.Spinner,
   110  			Stderr: g.Spinner,
   111  			Dir:    g.GitPath,
   112  		}
   113  
   114  		message.Command("git %s", strings.Join(fetchArgs, " "))
   115  
   116  		_, _, err := exec.CmdWithContext(context.TODO(), fetchExecConfig, "git", fetchArgs...)
   117  		if err != nil {
   118  			return err
   119  		}
   120  	}
   121  
   122  	return nil
   123  }