From 710640780f29a2a101769058c4fc4dbe23a06d3f Mon Sep 17 00:00:00 2001 From: Igor Ignatyev Date: Wed, 27 Nov 2024 18:11:11 +0300 Subject: [PATCH] #60: Composing with outdated cache --- compose/git.go | 227 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 171 insertions(+), 56 deletions(-) diff --git a/compose/git.go b/compose/git.go index 94e564b..333aceb 100644 --- a/compose/git.go +++ b/compose/git.go @@ -6,8 +6,9 @@ import ( "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" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/transport" "github.com/go-git/go-git/v5/plumbing/transport/http" @@ -23,6 +24,95 @@ func newGit(kw *keyringWrapper) Downloader { return &gitDownloader{k: kw} } +func (g *gitDownloader) fetchRemotes(r *git.Repository, url string, refSpec []config.RefSpec) error { + remotes, errR := r.Remotes() + if errR != nil { + return errR + } + + launchr.Term().Printfln("Fetching remote %s", url) + for _, rem := range remotes { + options := git.FetchOptions{ + //RefSpecs: []config.RefSpec{"refs/*:refs/*", "HEAD:refs/heads/HEAD"}, + RefSpecs: refSpec, + Force: true, + } + + auths := []authorizationMode{authorisationNone, authorisationKeyring, authorisationManual} + for _, authType := range auths { + if authType == authorisationNone { + err := rem.Fetch(&options) + if err != nil { + if errors.Is(err, transport.ErrAuthenticationRequired) { + continue + } + + if !errors.Is(err, git.NoErrAlreadyUpToDate) { + return err + } + + return nil + } + } + + if authType == authorisationKeyring { + ci, err := g.k.getForURL(url) + if err != nil { + return err + } + + options.Auth = &http.BasicAuth{ + Username: ci.Username, + Password: ci.Password, + } + + err = rem.Fetch(&options) + if err != nil { + if errors.Is(err, transport.ErrAuthorizationFailed) || errors.Is(err, transport.ErrAuthenticationRequired) { + if g.k.interactive { + launchr.Term().Println("invalid auth, trying manual authorisation") + continue + } + } + + if !errors.Is(err, git.NoErrAlreadyUpToDate) { + return err + } + + return nil + } + } + + if authType == authorisationManual { + ci := keyring.CredentialsItem{} + ci.URL = url + ci, err := g.k.fillCredentials(ci) + if err != nil { + return err + } + + options.Auth = &http.BasicAuth{ + Username: ci.Username, + Password: ci.Password, + } + + err = rem.Fetch(&options) + if err != nil { + if !errors.Is(err, git.NoErrAlreadyUpToDate) { + return err + } + + return nil + } + } + + break + } + } + + return nil +} + func (g *gitDownloader) EnsureLatest(pkg *Package, downloadPath string) (bool, error) { if _, err := os.Stat(downloadPath); os.IsNotExist(err) { // Return False in case package doesn't exist. @@ -31,87 +121,112 @@ func (g *gitDownloader) EnsureLatest(pkg *Package, downloadPath string) (bool, e r, err := git.PlainOpen(downloadPath) if err != nil { + launchr.Log().Debug("git init error", "err", err) + return false, nil + } + + head, err := r.Head() + if err != nil { + launchr.Log().Debug("get head error", "err", err) return false, err } - k, err := g.k.getForURL(pkg.GetURL()) + headName := head.Name().Short() + pkgRefName := pkg.GetRef() + remoteRefName := pkgRefName + + if pkg.GetTarget() == TargetLatest && headName != "" { + pkgRefName = headName + remoteRefName = plumbing.HEAD.String() + } + + isLatest := false + if headName == pkgRefName { + isLatest, err = g.ensureLatestBranch(r, pkg.GetURL(), pkgRefName, remoteRefName) + if err != nil { + launchr.Term().Warning().Printfln("Couldn't check local branch, marking package %s(%s) as outdated, see debug for detailed error.", pkg.GetName(), pkgRefName) + launchr.Log().Debug("ensure branch error", "err", err) + return isLatest, nil + } + } else { + isLatest, err = g.ensureLatestTag(r, pkg.GetURL(), pkgRefName) + if err != nil { + launchr.Term().Warning().Printfln("Couldn't check local tag, marking package %s(%s) as outdated, see debug for detailed error.", pkg.GetName(), pkgRefName) + launchr.Log().Debug("ensure tag error", "err", err) + return isLatest, nil + } + } + + if !isLatest { + launchr.Term().Warning().Printfln("Package %s(%s) is outdated", pkg.GetName(), pkgRefName) + } + + return isLatest, nil +} + +func (g *gitDownloader) ensureLatestBranch(r *git.Repository, fetchURL, refName, remoteRefName string) (bool, error) { + refSpec := []config.RefSpec{config.RefSpec(fmt.Sprintf("refs/heads/%s:refs/heads/%s", refName, refName))} + err := g.fetchRemotes(r, fetchURL, refSpec) if err != nil { return false, err } - remotes, err := r.Remotes() + br, err := r.Branch(refName) if err != nil { return false, err } - // Fetch from the remote repository - for _, rem := range remotes { - err = rem.Fetch(&git.FetchOptions{ - RefSpecs: []config.RefSpec{"refs/*:refs/*", "HEAD:refs/heads/HEAD"}, - Auth: &http.BasicAuth{ - Username: k.Username, - Password: k.Password, - }, - }) - if err != nil { - if !errors.Is(err, git.NoErrAlreadyUpToDate) { - return false, err - } - } + localRef, err := r.Reference(plumbing.ReferenceName(br.Merge.String()), true) + if err != nil { + return false, err } - head, err := r.Head() + remote := filepath.Join("refs", "remotes", br.Remote, remoteRefName) + remoteRef, err := r.Reference(plumbing.ReferenceName(remote), false) if err != nil { return false, err } - refName := head.Name().Short() - pkgRef := pkg.GetRef() - remoteRefName := pkgRef + return localRef.Hash() == remoteRef.Hash(), nil +} - if pkg.GetTarget() == TargetLatest && refName != "" { - pkgRef = refName - remoteRefName = plumbing.HEAD.String() +func (g *gitDownloader) ensureLatestTag(r *git.Repository, fetchURL, refName string) (bool, error) { + oldTag, err := r.Tag(refName) + if err != nil { + return false, err } - latest := false - if refName == pkgRef { - br, err := r.Branch(pkgRef) - if err != nil { - return false, err - } + head, err := r.Head() + if err != nil { + return false, err + } - localRef, err := r.Reference(plumbing.ReferenceName(br.Merge.String()), true) - if err != nil { - return false, err - } + refSpec := []config.RefSpec{config.RefSpec(fmt.Sprintf("refs/tags/%s:refs/tags/%s", refName, refName))} + err = g.fetchRemotes(r, fetchURL, refSpec) + if err != nil { + return false, err + } - // Obtain remote branch reference - remote := filepath.Join("refs", "remotes", br.Remote, remoteRefName) - remoteRef, err := r.Reference(plumbing.ReferenceName(remote), false) - if err != nil { - return false, err - } + newTag, err := r.Tag(refName) + if err != nil { + return false, err + } - if localRef.Hash() == remoteRef.Hash() { - latest = true - } else { - launchr.Term().Info().Println("Local and remote branches are not synced.") - } - } else { - tag, err := r.Tag(pkgRef) - if err != nil { - return false, err - } + if oldTag.Hash().String() != newTag.Hash().String() { + return false, err + } + revision := plumbing.Revision(newTag.Name().String()) + tagCommitHash, err := r.ResolveRevision(revision) + if err != nil { + return false, err + } - if tag.Hash() == head.Hash() { - latest = true - } else { - launchr.Term().Info().Println("HEAD and tag hashes do not match.") - } + commit, err := r.CommitObject(*tagCommitHash) + if err != nil { + return false, err } - return latest, nil + return commit.ID() == head.Hash(), nil } // Download implements Downloader.Download interface