Path: blob/main/component/module/git/internal/vcs/git.go
4096 views
package vcs12import (3"context"4"errors"5"io"6"os"7"path/filepath"89"github.com/go-git/go-git/v5"10"github.com/go-git/go-git/v5/plumbing"11)1213type GitRepoOptions struct {14Repository string15Revision string16}1718// GitRepo manages a Git repository for the purposes of retrieving a file from19// it.20type GitRepo struct {21opts GitRepoOptions22repo *git.Repository23workTree *git.Worktree24}2526// NewGitRepo creates a new instance of a GitRepo, where the Git repository is27// managed at storagePath.28//29// If storagePath is empty on disk, NewGitRepo initializes GitRepo by cloning30// the repository. Otherwise, NewGitRepo will do a fetch.31//32// After GitRepo is initialized, it checks out to the Revision specified in33// GitRepoOptions.34func NewGitRepo(ctx context.Context, storagePath string, opts GitRepoOptions) (*GitRepo, error) {35var (36repo *git.Repository37err error38)3940if !isRepoCloned(storagePath) {41repo, err = git.PlainCloneContext(ctx, storagePath, false, &git.CloneOptions{42URL: opts.Repository,43ReferenceName: plumbing.HEAD,44RecurseSubmodules: git.DefaultSubmoduleRecursionDepth,45Tags: git.AllTags,46})47} else {48repo, err = git.PlainOpen(storagePath)49}50if err != nil {51return nil, DownloadFailedError{52Repository: opts.Repository,53Inner: err,54}55}5657// Fetch the latest contents. This may be a no-op if we just did a clone.58err = repo.FetchContext(ctx, &git.FetchOptions{59RemoteName: "origin",60Force: true,61})62if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {63return nil, UpdateFailedError{64Repository: opts.Repository,65Inner: err,66}67}6869// Finally, hard reset to our requested revision.70hash, err := findRevision(opts.Revision, repo)71if err != nil {72return nil, InvalidRevisionError{Revision: opts.Revision}73}7475workTree, err := repo.Worktree()76if err != nil {77return nil, err78}79err = workTree.Reset(&git.ResetOptions{80Commit: hash,81Mode: git.HardReset,82})83if err != nil {84return nil, err85}8687return &GitRepo{88opts: opts,89repo: repo,90workTree: workTree,91}, nil92}9394func isRepoCloned(dir string) bool {95fi, dirError := os.ReadDir(filepath.Join(dir, git.GitDirName))96return dirError == nil && len(fi) > 097}9899// Update updates the repository by fetching new content and re-checking out to100// latest version of Revision.101func (repo *GitRepo) Update(ctx context.Context) error {102err := repo.repo.FetchContext(ctx, &git.FetchOptions{103RemoteName: "origin",104Force: true,105})106if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {107return UpdateFailedError{108Repository: repo.opts.Repository,109Inner: err,110}111}112113// Find the latest revision being requested and hard-reset to it.114hash, err := findRevision(repo.opts.Revision, repo.repo)115if err != nil {116return InvalidRevisionError{Revision: repo.opts.Revision}117}118119err = repo.workTree.Reset(&git.ResetOptions{120Commit: hash,121Mode: git.HardReset,122})123if err != nil {124return err125}126127return nil128}129130// ReadFile returns a file from the repository specified by path.131func (repo *GitRepo) ReadFile(path string) ([]byte, error) {132f, err := repo.workTree.Filesystem.Open(path)133if err != nil {134return nil, err135}136defer f.Close()137138return io.ReadAll(f)139}140141// CurrentRevision returns the current revision of the repository (by SHA).142func (repo *GitRepo) CurrentRevision() (string, error) {143ref, err := repo.repo.Head()144if err != nil {145return "", err146}147return ref.Hash().String(), nil148}149150func findRevision(rev string, repo *git.Repository) (plumbing.Hash, error) {151// Try looking for the revision in the following order:152//153// 1. Search by tag name.154// 2. Search by remote ref name.155// 3. Try to resolve the revision directly.156157if tagRef, err := repo.Tag(rev); err == nil {158return tagRef.Hash(), nil159}160161if remoteRef, err := repo.Reference(plumbing.NewRemoteReferenceName("origin", rev), true); err == nil {162return remoteRef.Hash(), nil163}164165if hash, err := repo.ResolveRevision(plumbing.Revision(rev)); err == nil {166return *hash, nil167}168169return plumbing.ZeroHash, plumbing.ErrReferenceNotFound170}171172173