1package main
  2
  3import (
  4	"errors"
  5	"os"
  6	"sort"
  7	"strings"
  8
  9	"github.com/go-git/go-git/v5"
 10	"github.com/go-git/go-git/v5/plumbing"
 11	"github.com/go-git/go-git/v5/plumbing/transport"
 12)
 13
 14// listRemoteTags lists all tags in a remote repository, whether HTTP(S) or SSH.
 15func listRemoteTags(url string) (tags []string, err error) {
 16	// TODO: Implement listRemoteTags
 17	// https://pkg.go.dev/github.com/go-git/go-git/v5@v5.8.0#NewRemote
 18	return nil, nil
 19}
 20
 21// fetchReleases fetches all releases in a remote repository, whether HTTP(S) or SSH.
 22func getGitReleases(p project) (project, error) {
 23	r, err := minimalClone(p.URL)
 24	if err != nil {
 25		return p, err
 26	}
 27	tagRefs, err := r.Tags()
 28	if err != nil {
 29		return p, err
 30	}
 31	err = tagRefs.ForEach(func(tagRef *plumbing.Reference) error {
 32		obj, err := r.TagObject(tagRef.Hash())
 33		switch err {
 34		case plumbing.ErrObjectNotFound:
 35			// This is a lightweight tag, not an annotated tag, skip it
 36			return nil
 37		case nil:
 38			url := ""
 39			tagName := bmStrict.Sanitize(tagRef.Name().Short())
 40			switch p.Forge {
 41			case "sourcehut":
 42				url = p.URL + "/refs/" + tagName
 43			case "gitlab":
 44				url = p.URL + "/-/releases/" + tagName
 45			default:
 46				url = ""
 47			}
 48			p.Releases = append(p.Releases, release{
 49				Tag:     tagName,
 50				Content: bmUGC.Sanitize(obj.Message),
 51				URL:     url,
 52				Date:    obj.Tagger.When,
 53			})
 54		default:
 55			return err
 56		}
 57		return nil
 58	})
 59	if err != nil {
 60		return p, err
 61	}
 62
 63	sort.Slice(p.Releases, func(i, j int) bool { return p.Releases[i].Date.After(p.Releases[j].Date) })
 64
 65	return p, nil
 66}
 67
 68// minimalClone clones a repository with a depth of 1 and no checkout.
 69func minimalClone(url string) (r *git.Repository, err error) {
 70	path, err := stringifyRepo(url)
 71	if err != nil {
 72		return nil, err
 73	}
 74
 75	if _, err := os.Stat(path); err == nil {
 76		r, err := git.PlainOpen(path)
 77		if err != nil {
 78			return nil, err
 79		}
 80		err = r.Fetch(&git.FetchOptions{
 81			RemoteName: "origin",
 82			Depth:      1,
 83			Tags:       git.AllTags,
 84		})
 85		if err == git.NoErrAlreadyUpToDate {
 86			return r, nil
 87		}
 88		return r, err
 89	}
 90
 91	r, err = git.PlainClone(path, false, &git.CloneOptions{
 92		URL:          url,
 93		SingleBranch: true,
 94		NoCheckout:   true,
 95		Depth:        1,
 96	})
 97	return r, err
 98}
 99
100// removeRepo removes a repository from the local filesystem.
101func removeRepo(url string) (err error) {
102	path, err := stringifyRepo(url)
103	if err != nil {
104		return err
105	}
106	err = os.RemoveAll(path)
107	return err
108}
109
110// stringifyRepo accepts a repository URI string and the corresponding local
111// filesystem path, whether the URI is HTTP, HTTPS, or SSH.
112func stringifyRepo(url string) (path string, err error) {
113	ep, err := transport.NewEndpoint(url)
114	if err != nil {
115		return "", err
116	}
117
118	if ep.Protocol == "http" || ep.Protocol == "https" {
119		return "data/" + strings.Split(url, "://")[1], nil
120	} else if ep.Protocol == "ssh" {
121		return "data/" + ep.Host + ep.Path, nil
122	} else {
123		return "", errors.New("unsupported protocol")
124	}
125}