github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/internal/packager/git/push.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  	"errors"
     9  	"fmt"
    10  	"os"
    11  	"path"
    12  
    13  	"github.com/Racer159/jackal/src/pkg/message"
    14  	"github.com/Racer159/jackal/src/pkg/transform"
    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  	"github.com/go-git/go-git/v5/plumbing/transport"
    19  	"github.com/go-git/go-git/v5/plumbing/transport/http"
    20  )
    21  
    22  // PushRepo pushes a git repository from the local path to the configured git server.
    23  func (g *Git) PushRepo(srcURL, targetFolder string) error {
    24  	spinner := message.NewProgressSpinner("Processing git repo %s", srcURL)
    25  	defer spinner.Stop()
    26  
    27  	// Setup git paths, including a unique name for the repo based on the hash of the git URL to avoid conflicts.
    28  	repoFolder, err := transform.GitURLtoFolderName(srcURL)
    29  	if err != nil {
    30  		return fmt.Errorf("unable to parse git url (%s): %w", srcURL, err)
    31  	}
    32  	repoPath := path.Join(targetFolder, repoFolder)
    33  
    34  	// Check that this package is using the new repo format (if not fallback to the format from <= 0.24.x)
    35  	_, err = os.Stat(repoPath)
    36  	if os.IsNotExist(err) {
    37  		repoFolder, err = transform.GitURLtoRepoName(srcURL)
    38  		if err != nil {
    39  			return fmt.Errorf("unable to parse git url (%s): %w", srcURL, err)
    40  		}
    41  		repoPath = path.Join(targetFolder, repoFolder)
    42  	}
    43  
    44  	g.GitPath = repoPath
    45  
    46  	repo, err := g.prepRepoForPush()
    47  	if err != nil {
    48  		message.Warnf("error when prepping the repo for push.. %v", err)
    49  		return err
    50  	}
    51  
    52  	if err := g.push(repo, spinner); err != nil {
    53  		return fmt.Errorf("failed to push the git repo %q: %w", repoFolder, err)
    54  	}
    55  
    56  	// Add the read-only user to this repo
    57  	if g.Server.InternalServer {
    58  		// Get the upstream URL
    59  		remote, err := repo.Remote(onlineRemoteName)
    60  		if err != nil {
    61  			message.Warn("unable to get the information needed to add the read-only user to the repo")
    62  			return err
    63  		}
    64  		remoteURL := remote.Config().URLs[0]
    65  		repoName, err := transform.GitURLtoRepoName(remoteURL)
    66  		if err != nil {
    67  			message.Warnf("Unable to add the read-only user to the repo: %s\n", repoName)
    68  			return err
    69  		}
    70  
    71  		err = g.addReadOnlyUserToRepo(g.Server.Address, repoName)
    72  		if err != nil {
    73  			message.Warnf("Unable to add the read-only user to the repo: %s\n", repoName)
    74  			return err
    75  		}
    76  	}
    77  
    78  	spinner.Success()
    79  	return nil
    80  }
    81  
    82  func (g *Git) prepRepoForPush() (*git.Repository, error) {
    83  	// Open the given repo
    84  	repo, err := git.PlainOpen(g.GitPath)
    85  	if err != nil {
    86  		return nil, fmt.Errorf("not a valid git repo or unable to open: %w", err)
    87  	}
    88  
    89  	// Get the upstream URL
    90  	remote, err := repo.Remote(onlineRemoteName)
    91  	if err != nil {
    92  		return nil, fmt.Errorf("unable to find the git remote: %w", err)
    93  	}
    94  
    95  	remoteURL := remote.Config().URLs[0]
    96  	targetURL, err := transform.GitURL(g.Server.Address, remoteURL, g.Server.PushUsername)
    97  	if err != nil {
    98  		return nil, fmt.Errorf("unable to transform the git url: %w", err)
    99  	}
   100  	message.Debugf("Rewrite git URL: %s -> %s", remoteURL, targetURL.String())
   101  	// Remove any preexisting offlineRemotes (happens when a retry is triggered)
   102  	_ = repo.DeleteRemote(offlineRemoteName)
   103  
   104  	_, err = repo.CreateRemote(&goConfig.RemoteConfig{
   105  		Name: offlineRemoteName,
   106  		URLs: []string{targetURL.String()},
   107  	})
   108  	if err != nil {
   109  		return nil, fmt.Errorf("failed to create offline remote: %w", err)
   110  	}
   111  
   112  	return repo, nil
   113  }
   114  
   115  func (g *Git) push(repo *git.Repository, spinner *message.Spinner) error {
   116  	gitCred := http.BasicAuth{
   117  		Username: g.Server.PushUsername,
   118  		Password: g.Server.PushPassword,
   119  	}
   120  
   121  	// Fetch remote offline refs in case of old update or if multiple refs are specified in one package
   122  	fetchOptions := &git.FetchOptions{
   123  		RemoteName: offlineRemoteName,
   124  		Auth:       &gitCred,
   125  		RefSpecs: []goConfig.RefSpec{
   126  			"refs/heads/*:refs/heads/*",
   127  			"refs/tags/*:refs/tags/*",
   128  		},
   129  	}
   130  
   131  	// Attempt the fetch, if it fails, log a warning and continue trying to push (might as well try..)
   132  	err := repo.Fetch(fetchOptions)
   133  	if errors.Is(err, transport.ErrRepositoryNotFound) {
   134  		message.Debugf("Repo not yet available offline, skipping fetch...")
   135  	} else if errors.Is(err, git.ErrForceNeeded) {
   136  		message.Debugf("Repo fetch requires force, skipping fetch...")
   137  	} else if errors.Is(err, git.NoErrAlreadyUpToDate) {
   138  		message.Debugf("Repo already up-to-date, skipping fetch...")
   139  	} else if err != nil {
   140  		return fmt.Errorf("unable to fetch the git repo prior to push: %w", err)
   141  	}
   142  
   143  	// Push all heads and tags to the offline remote
   144  	err = repo.Push(&git.PushOptions{
   145  		RemoteName: offlineRemoteName,
   146  		Auth:       &gitCred,
   147  		Progress:   spinner,
   148  		// TODO: (@JEFFMCCOY) add the parsing for the `+` force prefix (see https://github.com/Racer159/jackal/issues/1410)
   149  		//Force: isForce,
   150  		// If a provided refspec doesn't push anything, it is just ignored
   151  		RefSpecs: []goConfig.RefSpec{
   152  			"refs/heads/*:refs/heads/*",
   153  			"refs/tags/*:refs/tags/*",
   154  		},
   155  	})
   156  
   157  	if errors.Is(err, git.NoErrAlreadyUpToDate) {
   158  		message.Debug("Repo already up-to-date")
   159  	} else if errors.Is(err, plumbing.ErrObjectNotFound) {
   160  		return fmt.Errorf("unable to push repo due to likely shallow clone: %s", err.Error())
   161  	} else if err != nil {
   162  		return fmt.Errorf("unable to push repo to the gitops service: %s", err.Error())
   163  	}
   164  
   165  	return nil
   166  }