git.go

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