Browse Source

feat(vcs): package vcs

jguer 4 years ago
parent
commit
c5af6f8189
2 changed files with 295 additions and 0 deletions
  1. 253 0
      pkg/vcs/vcs.go
  2. 42 0
      pkg/vcs/vcs_test.go

+ 253 - 0
pkg/vcs/vcs.go

@@ -0,0 +1,253 @@
+package vcs
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+	"strings"
+	"sync"
+
+	gosrc "github.com/Morganamilo/go-srcinfo"
+	"github.com/leonelquinteros/gotext"
+
+	"github.com/Jguer/yay/v10/pkg/settings/exe"
+	"github.com/Jguer/yay/v10/pkg/text"
+)
+
+// InfoStore is a collection of OriginInfoByURL by Package.
+// Containing a map of last commit SHAs of a repo
+type InfoStore struct {
+	OriginsByPackage map[string]OriginInfoByURL
+	FilePath         string
+	Runner           exe.Runner
+	CmdBuilder       *exe.CmdBuilder
+}
+
+// OriginInfoByURL stores the OriginInfo of each origin URL provided
+type OriginInfoByURL map[string]OriginInfo
+
+// OriginInfo contains the last commit sha of a repo
+// Example:
+// "github.com/Jguer/yay.git": {
+// 	"protocols": [
+// 		"https"
+// 	],
+// 	"branch": "next",
+// 	"sha": "c1171d41467c68ffd3c46748182a16366aaaf87b"
+// }
+type OriginInfo struct {
+	Protocols []string `json:"protocols"`
+	Branch    string   `json:"branch"`
+	SHA       string   `json:"sha"`
+}
+
+func NewInfoStore(filePath string, runner exe.Runner, cmdBuilder *exe.CmdBuilder) *InfoStore {
+	infoStore := &InfoStore{
+		CmdBuilder:       cmdBuilder,
+		FilePath:         filePath,
+		OriginsByPackage: map[string]OriginInfoByURL{},
+		Runner:           runner}
+
+	return infoStore
+}
+
+// GetCommit parses HEAD commit from url and branch
+func (v *InfoStore) getCommit(url, branch string, protocols []string) string {
+	if len(protocols) > 0 {
+		protocol := protocols[len(protocols)-1]
+
+		cmd := v.CmdBuilder.BuildGitCmd("", "ls-remote", protocol+"://"+url, branch)
+		stdout, _, err := v.Runner.Capture(cmd, 5)
+		if err != nil {
+			text.Warnln(err)
+			return ""
+		}
+
+		split := strings.Fields(stdout)
+
+		if len(split) < 2 {
+			return ""
+		}
+
+		commit := split[0]
+		return commit
+	}
+
+	return ""
+}
+
+func (v *InfoStore) Update(pkgName string, sources []gosrc.ArchString, mux sync.Locker, wg *sync.WaitGroup) {
+	defer wg.Done()
+
+	info := make(OriginInfoByURL)
+	checkSource := func(source gosrc.ArchString) {
+		defer wg.Done()
+		url, branch, protocols := parseSource(source.Value)
+		if url == "" || branch == "" {
+			return
+		}
+
+		commit := v.getCommit(url, branch, protocols)
+		if commit == "" {
+			return
+		}
+
+		mux.Lock()
+		info[url] = OriginInfo{
+			protocols,
+			branch,
+			commit,
+		}
+
+		v.OriginsByPackage[pkgName] = info
+		text.Warnln(gotext.Get("Found git repo: %s", text.Cyan(url)))
+
+		if err := v.Save(); err != nil {
+			fmt.Fprintln(os.Stderr, err)
+		}
+		mux.Unlock()
+	}
+
+	for _, source := range sources {
+		wg.Add(1)
+		go checkSource(source)
+	}
+}
+
+// parseSource returns the git url, default branch and protocols it supports
+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
+	}
+	protocols = strings.SplitN(split[0], "+", 2)
+
+	git := false
+	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
+}
+
+func (v *InfoStore) NeedsUpdate(infos OriginInfoByURL) bool {
+	// 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{})
+
+	checkHash := func(url string, info OriginInfo) {
+		hash := v.getCommit(url, info.Branch, info.Protocols)
+		if hash != "" && hash != info.SHA {
+			hasUpdate <- struct{}{}
+		} else {
+			finished <- struct{}{}
+		}
+	}
+
+	for url, info := range infos {
+		alive++
+		go checkHash(url, info)
+	}
+
+	for {
+		select {
+		case <-hasUpdate:
+			return true
+		case <-finished:
+			alive--
+			if alive == 0 {
+				return false
+			}
+		}
+	}
+}
+
+func (v *InfoStore) Save() error {
+	marshalledinfo, err := json.MarshalIndent(v, "", "\t")
+	if err != nil || string(marshalledinfo) == "null" {
+		return err
+	}
+	in, err := os.OpenFile(v.FilePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644)
+	if err != nil {
+		return err
+	}
+	defer in.Close()
+	_, err = in.Write(marshalledinfo)
+	if err != nil {
+		return err
+	}
+	err = in.Sync()
+	return err
+}
+
+// RemovePackage removes package from VCS information
+func (v *InfoStore) RemovePackage(pkgs []string) {
+	updated := false
+
+	for _, pkgName := range pkgs {
+		if _, ok := v.OriginsByPackage[pkgName]; ok {
+			delete(v.OriginsByPackage, pkgName)
+			updated = true
+		}
+	}
+
+	if updated {
+		if err := v.Save(); err != nil {
+			fmt.Fprintln(os.Stderr, err)
+		}
+	}
+}
+
+// LoadStore reads a json file and populates a InfoStore structure
+func (v InfoStore) Load() error {
+	vfile, err := os.Open(v.FilePath)
+	if !os.IsNotExist(err) && err != nil {
+		return fmt.Errorf("failed to open vcs file '%s': %s", v.FilePath, err)
+	}
+
+	defer vfile.Close()
+	if !os.IsNotExist(err) {
+		decoder := json.NewDecoder(vfile)
+		if err = decoder.Decode(&v.OriginsByPackage); err != nil {
+			return fmt.Errorf("failed to read vcs '%s': %s", v.FilePath, err)
+		}
+	}
+
+	return nil
+}

+ 42 - 0
pkg/vcs/vcs_test.go

@@ -0,0 +1,42 @@
+package vcs
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestParsing(t *testing.T) {
+	type source struct {
+		URL       string
+		Branch    string
+		Protocols []string
+	}
+
+	urls := []string{
+		"git+https://github.com/neovim/neovim.git",
+		"git://github.com/jguer/yay.git#branch=master",
+		"git://github.com/davidgiven/ack",
+		"git://github.com/jguer/yay.git#tag=v3.440",
+		"git://github.com/jguer/yay.git#commit=e5470c88c6e2f9e0f97deb4728659ffa70ef5d0c",
+		"a+b+c+d+e+f://github.com/jguer/yay.git#branch=foo",
+	}
+
+	sources := []source{
+		{"github.com/neovim/neovim.git", "HEAD", []string{"https"}},
+		{"github.com/jguer/yay.git", "master", []string{"git"}},
+		{"github.com/davidgiven/ack", "HEAD", []string{"git"}},
+		{"", "", nil},
+		{"", "", nil},
+		{"", "", nil},
+	}
+
+	for n, url := range urls {
+		url, branch, protocols := parseSource(url)
+		compare := sources[n]
+
+		assert.Equal(t, compare.URL, url)
+		assert.Equal(t, compare.Branch, branch)
+		assert.Equal(t, compare.Protocols, protocols)
+	}
+}