Files
agent-lib/internal/client/pull.go
T

140 lines
4.0 KiB
Go
Raw Permalink Normal View History

package client
import (
"errors"
"fmt"
"os"
"path/filepath"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
gittransport "github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/m3tam3re/agent-lib/internal/gitsource"
)
// PullResult is one successful work-repo pull: the resolved revision plus
// read access to its commit tree.
type PullResult struct {
Rev string
Tree *gitsource.GitTree
}
// Pull fetches the work repository into the cache dir (bare clone, then
// fetch) and resolves ref — the default branch when empty. An unreachable
// repository yields a clean error before any deployment state is touched.
func Pull(cacheDir, url, ref, token, tokenUser string) (*PullResult, error) {
auth := basicAuth(token, tokenUser)
repo, err := openOrCreate(cacheDir, url, ref, auth)
if err != nil {
return nil, err
}
hash, err := resolveRemoteRef(repo, ref)
if err != nil {
return nil, fmt.Errorf("resolving %q in %s: %w", refLabel(ref), gitsource.RedactURL(url), err)
}
commit, err := repo.CommitObject(hash)
if err != nil {
return nil, fmt.Errorf("reading commit %s: %w", hash, err)
}
tree, err := commit.Tree()
if err != nil {
return nil, fmt.Errorf("reading tree of %s: %w", hash, err)
}
return &PullResult{Rev: hash.String(), Tree: gitsource.NewGitTree(tree)}, nil
}
func basicAuth(token, tokenUser string) gittransport.AuthMethod {
if token == "" {
return nil
}
user := tokenUser
if user == "" {
user = "agent-lib"
}
return &http.BasicAuth{Username: user, Password: token}
}
func openOrCreate(cacheDir, url, ref string, auth gittransport.AuthMethod) (*git.Repository, error) {
if _, err := os.Stat(filepath.Join(cacheDir, "HEAD")); err == nil {
repo, err := git.PlainOpen(cacheDir)
if err != nil {
return nil, fmt.Errorf("opening cached work repo: %w", err)
}
if err := repointRemote(repo, url); err != nil {
return nil, err
}
if err := repo.Fetch(&git.FetchOptions{Auth: auth}); err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
return nil, fmt.Errorf("fetching %s: %w", gitsource.RedactURL(url), err)
}
return repo, nil
}
if err := os.MkdirAll(filepath.Dir(cacheDir), 0o755); err != nil {
return nil, err
}
opts := &git.CloneOptions{URL: url, Auth: auth, NoCheckout: true}
if ref != "" {
opts.ReferenceName = plumbing.NewBranchReferenceName(ref)
}
repo, err := git.PlainClone(cacheDir, true, opts)
if err != nil {
os.RemoveAll(cacheDir)
return nil, fmt.Errorf("cloning %s: %w", gitsource.RedactURL(url), err)
}
return repo, nil
}
// repointRemote aligns the cached origin with the configured URL so a fleet
// re-point takes effect on the next sync.
func repointRemote(repo *git.Repository, url string) error {
remote, err := repo.Remote(git.DefaultRemoteName)
if err != nil {
return fmt.Errorf("reading cached remote: %w", err)
}
if len(remote.Config().URLs) == 0 || remote.Config().URLs[0] == url {
return nil
}
if err := repo.DeleteRemote(git.DefaultRemoteName); err != nil {
return fmt.Errorf("replacing cached remote: %w", err)
}
_, err = repo.CreateRemote(&config.RemoteConfig{
Name: git.DefaultRemoteName,
URLs: []string{url},
Fetch: []config.RefSpec{
config.RefSpec("+refs/heads/*:refs/remotes/origin/*"),
},
})
return err
}
func resolveRemoteRef(repo *git.Repository, ref string) (plumbing.Hash, error) {
candidates := remoteRefCandidates(ref)
for _, name := range candidates {
if h, err := repo.ResolveRevision(plumbing.Revision(name)); err == nil {
return *h, nil
}
}
return plumbing.ZeroHash, fmt.Errorf("reference not found (tried %v)", candidates)
}
func remoteRefCandidates(ref string) []string {
if ref == "" {
return []string{"refs/remotes/origin/HEAD", "HEAD"}
}
return []string{
"refs/remotes/origin/" + ref,
"refs/tags/" + ref,
"refs/heads/" + ref,
ref,
}
}
func refLabel(ref string) string {
if ref == "" {
return "default branch"
}
return ref
}