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