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 }