2020-08-21 02:37:03 +02:00
|
|
|
package vcs
|
|
|
|
|
|
|
|
import (
|
2021-08-12 18:56:23 +02:00
|
|
|
"context"
|
2020-08-21 02:37:03 +02:00
|
|
|
"encoding/json"
|
2023-01-03 22:43:56 +01:00
|
|
|
"errors"
|
2020-08-21 02:37:03 +02:00
|
|
|
"fmt"
|
|
|
|
"os"
|
2021-07-03 17:06:20 +02:00
|
|
|
"os/exec"
|
2020-08-21 02:37:03 +02:00
|
|
|
"strings"
|
|
|
|
"sync"
|
2021-08-12 18:56:23 +02:00
|
|
|
"time"
|
2020-08-21 02:37:03 +02:00
|
|
|
|
2023-01-24 00:03:32 +01:00
|
|
|
"github.com/Jguer/go-alpm/v2"
|
2020-08-21 02:37:03 +02:00
|
|
|
gosrc "github.com/Morganamilo/go-srcinfo"
|
|
|
|
"github.com/leonelquinteros/gotext"
|
|
|
|
|
2021-09-08 22:28:08 +02:00
|
|
|
"github.com/Jguer/yay/v11/pkg/settings/exe"
|
|
|
|
"github.com/Jguer/yay/v11/pkg/text"
|
2020-08-21 02:37:03 +02:00
|
|
|
)
|
|
|
|
|
2022-12-16 18:23:44 +01:00
|
|
|
type Store interface {
|
2023-01-24 00:03:32 +01:00
|
|
|
// ToUpgrade returns true if the package needs to be updated.
|
|
|
|
ToUpgrade(ctx context.Context, pkgName string) bool
|
2022-12-18 17:37:15 +01:00
|
|
|
// Update updates the VCS info of a package.
|
|
|
|
Update(ctx context.Context, pkgName string, sources []gosrc.ArchString)
|
2023-01-24 00:03:32 +01:00
|
|
|
// RemovePackages removes the VCS info of the packages given as arg if they exist.
|
|
|
|
RemovePackages(pkgs []string)
|
|
|
|
// Clean orphaned VCS info.
|
|
|
|
CleanOrphans(pkgs map[string]alpm.IPackage)
|
2022-12-18 17:37:15 +01:00
|
|
|
// Load loads the VCS info from disk.
|
2022-12-16 18:23:44 +01:00
|
|
|
Load() error
|
2023-01-24 00:03:32 +01:00
|
|
|
// Save saves the VCS info to disk.
|
|
|
|
Save() error
|
2022-12-16 18:23:44 +01:00
|
|
|
}
|
|
|
|
|
2020-08-21 02:37:03 +02:00
|
|
|
// InfoStore is a collection of OriginInfoByURL by Package.
|
2021-08-11 20:13:28 +02:00
|
|
|
// Containing a map of last commit SHAs of a repo.
|
2020-08-21 02:37:03 +02:00
|
|
|
type InfoStore struct {
|
|
|
|
OriginsByPackage map[string]OriginInfoByURL
|
|
|
|
FilePath string
|
2021-08-08 09:28:53 +02:00
|
|
|
CmdBuilder exe.GitCmdBuilder
|
2022-12-18 17:37:15 +01:00
|
|
|
mux sync.Mutex
|
2023-02-25 20:03:27 +01:00
|
|
|
logger *text.Logger
|
2020-08-21 02:37:03 +02:00
|
|
|
}
|
|
|
|
|
2021-08-11 20:13:28 +02:00
|
|
|
// OriginInfoByURL stores the OriginInfo of each origin URL provided.
|
2020-08-21 02:37:03 +02:00
|
|
|
type OriginInfoByURL map[string]OriginInfo
|
|
|
|
|
|
|
|
// OriginInfo contains the last commit sha of a repo
|
|
|
|
// Example:
|
2022-08-05 18:56:28 +02:00
|
|
|
//
|
|
|
|
// "github.com/Jguer/yay.git": {
|
|
|
|
// "protocols": [
|
|
|
|
// "https"
|
|
|
|
// ],
|
|
|
|
// "branch": "next",
|
|
|
|
// "sha": "c1171d41467c68ffd3c46748182a16366aaaf87b"
|
|
|
|
// }.
|
2020-08-21 02:37:03 +02:00
|
|
|
type OriginInfo struct {
|
|
|
|
Protocols []string `json:"protocols"`
|
|
|
|
Branch string `json:"branch"`
|
|
|
|
SHA string `json:"sha"`
|
|
|
|
}
|
|
|
|
|
2023-02-25 20:03:27 +01:00
|
|
|
func NewInfoStore(filePath string, cmdBuilder exe.GitCmdBuilder,
|
|
|
|
logger *text.Logger,
|
|
|
|
) *InfoStore {
|
2020-08-21 02:37:03 +02:00
|
|
|
infoStore := &InfoStore{
|
|
|
|
CmdBuilder: cmdBuilder,
|
|
|
|
FilePath: filePath,
|
|
|
|
OriginsByPackage: map[string]OriginInfoByURL{},
|
2022-12-18 17:37:15 +01:00
|
|
|
mux: sync.Mutex{},
|
2023-02-25 20:03:27 +01:00
|
|
|
logger: logger,
|
2020-08-21 02:39:52 +02:00
|
|
|
}
|
2020-08-21 02:37:03 +02:00
|
|
|
|
|
|
|
return infoStore
|
|
|
|
}
|
|
|
|
|
2021-08-11 20:13:28 +02:00
|
|
|
// GetCommit parses HEAD commit from url and branch.
|
2021-08-12 18:56:23 +02:00
|
|
|
func (v *InfoStore) getCommit(ctx context.Context, url, branch string, protocols []string) string {
|
2020-08-21 02:37:03 +02:00
|
|
|
if len(protocols) > 0 {
|
|
|
|
protocol := protocols[len(protocols)-1]
|
|
|
|
|
2021-08-12 18:56:23 +02:00
|
|
|
ctxTimeout, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
|
|
defer cancel()
|
2021-08-11 20:13:28 +02:00
|
|
|
|
2021-08-12 18:56:23 +02:00
|
|
|
cmd := v.CmdBuilder.BuildGitCmd(ctxTimeout, "", "ls-remote", protocol+"://"+url, branch)
|
|
|
|
|
|
|
|
stdout, _, err := v.CmdBuilder.Capture(cmd)
|
2020-08-21 02:37:03 +02:00
|
|
|
if err != nil {
|
2023-01-03 22:43:56 +01:00
|
|
|
exitError := &exec.ExitError{}
|
|
|
|
if ok := errors.As(err, &exitError); ok && exitError.ExitCode() == 128 {
|
2023-02-25 20:03:27 +01:00
|
|
|
v.logger.Warnln(gotext.Get("devel check for package failed: '%s' encountered an error", cmd.String()))
|
2021-07-03 17:06:20 +02:00
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2023-02-25 20:03:27 +01:00
|
|
|
v.logger.Warnln(err)
|
2021-08-11 20:13:28 +02:00
|
|
|
|
2020-08-21 02:37:03 +02:00
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
split := strings.Fields(stdout)
|
|
|
|
|
|
|
|
if len(split) < 2 {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
commit := split[0]
|
2021-08-11 20:13:28 +02:00
|
|
|
|
2020-08-21 02:37:03 +02:00
|
|
|
return commit
|
|
|
|
}
|
|
|
|
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2022-12-18 17:37:15 +01:00
|
|
|
func (v *InfoStore) Update(ctx context.Context, pkgName string, sources []gosrc.ArchString) {
|
|
|
|
var wg sync.WaitGroup
|
2020-08-21 02:37:03 +02:00
|
|
|
info := make(OriginInfoByURL)
|
|
|
|
checkSource := func(source gosrc.ArchString) {
|
|
|
|
defer wg.Done()
|
2021-08-11 20:13:28 +02:00
|
|
|
|
2020-08-21 02:37:03 +02:00
|
|
|
url, branch, protocols := parseSource(source.Value)
|
|
|
|
if url == "" || branch == "" {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-08-12 18:56:23 +02:00
|
|
|
commit := v.getCommit(ctx, url, branch, protocols)
|
2020-08-21 02:37:03 +02:00
|
|
|
if commit == "" {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-12-18 17:37:15 +01:00
|
|
|
v.mux.Lock()
|
2020-08-21 02:37:03 +02:00
|
|
|
info[url] = OriginInfo{
|
|
|
|
protocols,
|
|
|
|
branch,
|
|
|
|
commit,
|
|
|
|
}
|
|
|
|
|
|
|
|
v.OriginsByPackage[pkgName] = info
|
2021-08-11 20:13:28 +02:00
|
|
|
|
2023-02-25 20:03:27 +01:00
|
|
|
v.logger.Warnln(gotext.Get("Found git repo: %s", text.Cyan(url)))
|
2020-08-21 02:37:03 +02:00
|
|
|
|
|
|
|
if err := v.Save(); err != nil {
|
|
|
|
fmt.Fprintln(os.Stderr, err)
|
|
|
|
}
|
2022-12-18 17:37:15 +01:00
|
|
|
v.mux.Unlock()
|
2020-08-21 02:37:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, source := range sources {
|
|
|
|
wg.Add(1)
|
2021-08-11 20:13:28 +02:00
|
|
|
|
2020-08-21 02:37:03 +02:00
|
|
|
go checkSource(source)
|
|
|
|
}
|
2022-12-18 17:37:15 +01:00
|
|
|
|
|
|
|
wg.Wait()
|
2020-08-21 02:37:03 +02:00
|
|
|
}
|
|
|
|
|
2021-08-11 20:13:28 +02:00
|
|
|
// parseSource returns the git url, default branch and protocols it supports.
|
2020-08-21 02:37:03 +02:00
|
|
|
func parseSource(source string) (url, branch string, protocols []string) {
|
|
|
|
split := strings.Split(source, "::")
|
|
|
|
source = split[len(split)-1]
|
|
|
|
split = strings.SplitN(source, "://", 2)
|
|
|
|
|
|
|
|
if len(split) != 2 {
|
|
|
|
return "", "", nil
|
|
|
|
}
|
2021-08-11 20:13:28 +02:00
|
|
|
|
2020-08-21 02:37:03 +02:00
|
|
|
protocols = strings.SplitN(split[0], "+", 2)
|
|
|
|
|
|
|
|
git := false
|
2021-08-11 20:13:28 +02:00
|
|
|
|
2020-08-21 02:37:03 +02:00
|
|
|
for _, protocol := range protocols {
|
|
|
|
if protocol == "git" {
|
|
|
|
git = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protocols = protocols[len(protocols)-1:]
|
|
|
|
|
|
|
|
if !git {
|
|
|
|
return "", "", nil
|
|
|
|
}
|
|
|
|
|
|
|
|
split = strings.SplitN(split[1], "#", 2)
|
|
|
|
if len(split) == 2 {
|
|
|
|
secondSplit := strings.SplitN(split[1], "=", 2)
|
|
|
|
if secondSplit[0] != "branch" {
|
|
|
|
// source has #commit= or #tag= which makes them not vcs
|
|
|
|
// packages because they reference a specific point
|
|
|
|
return "", "", nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(secondSplit) == 2 {
|
|
|
|
url = split[0]
|
|
|
|
branch = secondSplit[1]
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
url = split[0]
|
|
|
|
branch = "HEAD"
|
|
|
|
}
|
|
|
|
|
|
|
|
url = strings.Split(url, "?")[0]
|
|
|
|
branch = strings.Split(branch, "?")[0]
|
|
|
|
|
|
|
|
return url, branch, protocols
|
|
|
|
}
|
|
|
|
|
2023-01-24 00:03:32 +01:00
|
|
|
func (v *InfoStore) ToUpgrade(ctx context.Context, pkgName string) bool {
|
|
|
|
if infos, ok := v.OriginsByPackage[pkgName]; ok {
|
|
|
|
return v.needsUpdate(ctx, infos)
|
2022-12-18 17:37:15 +01:00
|
|
|
}
|
|
|
|
|
2023-01-24 00:03:32 +01:00
|
|
|
return false
|
2022-12-18 17:37:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (v *InfoStore) needsUpdate(ctx context.Context, infos OriginInfoByURL) bool {
|
2020-08-21 02:37:03 +02:00
|
|
|
// used to signal we have gone through all sources and found nothing
|
|
|
|
finished := make(chan struct{})
|
|
|
|
alive := 0
|
|
|
|
|
|
|
|
// if we find an update we use this to exit early and return true
|
|
|
|
hasUpdate := make(chan struct{})
|
|
|
|
|
2021-02-18 16:46:40 +01:00
|
|
|
closed := make(chan struct{})
|
|
|
|
defer close(closed)
|
|
|
|
|
2020-08-21 02:37:03 +02:00
|
|
|
checkHash := func(url string, info OriginInfo) {
|
2021-08-12 18:56:23 +02:00
|
|
|
hash := v.getCommit(ctx, url, info.Branch, info.Protocols)
|
2021-02-18 16:46:40 +01:00
|
|
|
|
|
|
|
var sendTo chan<- struct{}
|
2020-08-21 02:37:03 +02:00
|
|
|
if hash != "" && hash != info.SHA {
|
2021-02-18 16:46:40 +01:00
|
|
|
sendTo = hasUpdate
|
2020-08-21 02:37:03 +02:00
|
|
|
} else {
|
2021-02-18 16:46:40 +01:00
|
|
|
sendTo = finished
|
|
|
|
}
|
|
|
|
|
|
|
|
select {
|
|
|
|
case sendTo <- struct{}{}:
|
|
|
|
case <-closed:
|
2020-08-21 02:37:03 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for url, info := range infos {
|
|
|
|
alive++
|
2021-08-11 20:13:28 +02:00
|
|
|
|
2020-08-21 02:37:03 +02:00
|
|
|
go checkHash(url, info)
|
|
|
|
}
|
|
|
|
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-hasUpdate:
|
|
|
|
return true
|
|
|
|
case <-finished:
|
|
|
|
alive--
|
|
|
|
if alive == 0 {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *InfoStore) Save() error {
|
2020-08-22 00:39:26 +02:00
|
|
|
marshalledinfo, err := json.MarshalIndent(v.OriginsByPackage, "", "\t")
|
2020-08-21 02:37:03 +02:00
|
|
|
if err != nil || string(marshalledinfo) == "null" {
|
|
|
|
return err
|
|
|
|
}
|
2021-08-11 20:13:28 +02:00
|
|
|
|
2020-08-21 02:37:03 +02:00
|
|
|
in, err := os.OpenFile(v.FilePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2021-08-11 20:13:28 +02:00
|
|
|
|
2020-08-21 02:37:03 +02:00
|
|
|
defer in.Close()
|
2021-08-11 20:13:28 +02:00
|
|
|
|
|
|
|
if _, errM := in.Write(marshalledinfo); errM != nil {
|
|
|
|
return errM
|
2020-08-21 02:37:03 +02:00
|
|
|
}
|
2021-08-11 20:13:28 +02:00
|
|
|
|
|
|
|
return in.Sync()
|
2020-08-21 02:37:03 +02:00
|
|
|
}
|
|
|
|
|
2021-08-11 20:13:28 +02:00
|
|
|
// RemovePackage removes package from VCS information.
|
2023-01-24 00:03:32 +01:00
|
|
|
func (v *InfoStore) RemovePackages(pkgs []string) {
|
2020-08-21 02:37:03 +02:00
|
|
|
updated := false
|
|
|
|
|
|
|
|
for _, pkgName := range pkgs {
|
|
|
|
if _, ok := v.OriginsByPackage[pkgName]; ok {
|
|
|
|
delete(v.OriginsByPackage, pkgName)
|
2021-08-11 20:13:28 +02:00
|
|
|
|
2020-08-21 02:37:03 +02:00
|
|
|
updated = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if updated {
|
|
|
|
if err := v.Save(); err != nil {
|
|
|
|
fmt.Fprintln(os.Stderr, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-11 20:13:28 +02:00
|
|
|
// LoadStore reads a json file and populates a InfoStore structure.
|
2022-12-18 17:37:15 +01:00
|
|
|
func (v *InfoStore) Load() error {
|
2020-08-21 02:37:03 +02:00
|
|
|
vfile, err := os.Open(v.FilePath)
|
|
|
|
if !os.IsNotExist(err) && err != nil {
|
2023-01-03 22:43:56 +01:00
|
|
|
return fmt.Errorf("failed to open vcs file '%s': %w", v.FilePath, err)
|
2020-08-21 02:37:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
defer vfile.Close()
|
2021-08-11 20:13:28 +02:00
|
|
|
|
2020-08-21 02:37:03 +02:00
|
|
|
if !os.IsNotExist(err) {
|
|
|
|
decoder := json.NewDecoder(vfile)
|
|
|
|
if err = decoder.Decode(&v.OriginsByPackage); err != nil {
|
2023-01-03 22:43:56 +01:00
|
|
|
return fmt.Errorf("failed to read vcs '%s': %w", v.FilePath, err)
|
2020-08-21 02:37:03 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2023-01-24 00:03:32 +01:00
|
|
|
|
|
|
|
func (v *InfoStore) CleanOrphans(pkgs map[string]alpm.IPackage) {
|
|
|
|
missing := make([]string, 0)
|
|
|
|
|
|
|
|
for pkgName := range v.OriginsByPackage {
|
|
|
|
if _, ok := pkgs[pkgName]; !ok {
|
2023-02-25 20:03:27 +01:00
|
|
|
v.logger.Debugln("removing orphaned vcs package:", pkgName)
|
2023-01-24 00:03:32 +01:00
|
|
|
missing = append(missing, pkgName)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
v.RemovePackages(missing)
|
|
|
|
}
|