git.go

  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	"strings"
 13	"time"
 14
 15	"github.com/microcosm-cc/bluemonday"
 16
 17	"github.com/go-git/go-git/v5"
 18	"github.com/go-git/go-git/v5/plumbing"
 19	"github.com/go-git/go-git/v5/plumbing/transport"
 20)
 21
 22type Release struct {
 23	Tag     string
 24	Content string
 25	URL     string
 26	Date    time.Time
 27}
 28
 29var (
 30	bmUGC    = bluemonday.UGCPolicy()
 31	bmStrict = bluemonday.StrictPolicy()
 32)
 33
 34// listRemoteTags lists all tags in a remote repository, whether HTTP(S) or SSH.
 35// func listRemoteTags(url string) (tags []string, err error) {
 36// 	// TODO: Implement listRemoteTags
 37// 	// https://pkg.go.dev/github.com/go-git/go-git/v5@v5.8.0#NewRemote
 38// 	return nil, nil
 39// }
 40
 41// GetReleases fetches all releases in a remote repository, whether HTTP(S) or
 42// SSH.
 43func GetReleases(gitURI, forge string) ([]Release, error) {
 44	r, err := minimalClone(gitURI)
 45	if err != nil {
 46		return nil, err
 47	}
 48	tagRefs, err := r.Tags()
 49	if err != nil {
 50		return nil, err
 51	}
 52
 53	parsedURI, err := url.Parse(gitURI)
 54	if err != nil {
 55		fmt.Println("Error parsing URI: " + err.Error())
 56	}
 57
 58	var httpURI string
 59	if parsedURI.Scheme != "" {
 60		httpURI = parsedURI.Host + parsedURI.Path
 61	}
 62
 63	releases := make([]Release, 0)
 64
 65	err = tagRefs.ForEach(func(tagRef *plumbing.Reference) error {
 66		tagObj, err := r.TagObject(tagRef.Hash())
 67
 68		var message string
 69		var date time.Time
 70		if errors.Is(err, plumbing.ErrObjectNotFound) {
 71			commitTag, err := r.CommitObject(tagRef.Hash())
 72			if err != nil {
 73				return err
 74			}
 75			message = commitTag.Message
 76			date = commitTag.Committer.When
 77		} else {
 78			message = tagObj.Message
 79			date = tagObj.Tagger.When
 80		}
 81
 82		tagURL := ""
 83		tagName := bmStrict.Sanitize(tagRef.Name().Short())
 84		switch forge {
 85		case "sourcehut":
 86			tagURL = "https://" + httpURI + "/refs/" + tagName
 87		case "gitlab":
 88			tagURL = "https://" + httpURI + "/-/releases/" + tagName
 89		default:
 90			tagURL = ""
 91		}
 92
 93		releases = append(releases, Release{
 94			Tag:     tagName,
 95			Content: bmUGC.Sanitize(message),
 96			URL:     tagURL,
 97			Date:    date,
 98		})
 99		return nil
100	})
101	if err != nil {
102		return nil, err
103	}
104
105	return releases, nil
106}
107
108// minimalClone clones a repository with a depth of 1 and no checkout.
109func minimalClone(url string) (r *git.Repository, err error) {
110	path, err := stringifyRepo(url)
111	if err != nil {
112		return nil, err
113	}
114
115	r, err = git.PlainOpen(path)
116	if err == nil {
117		err = r.Fetch(&git.FetchOptions{
118			RemoteName: "origin",
119			Depth:      1,
120			Tags:       git.AllTags,
121		})
122		return r, err
123	} else if !errors.Is(err, git.ErrRepositoryNotExists) {
124		return nil, err
125	}
126
127	r, err = git.PlainClone(path, false, &git.CloneOptions{
128		URL:          url,
129		SingleBranch: true,
130		NoCheckout:   true,
131		Depth:        1,
132	})
133	return r, err
134}
135
136// RemoveRepo removes a repository from the local filesystem.
137func RemoveRepo(url string) (err error) {
138	path, err := stringifyRepo(url)
139	if err != nil {
140		return err
141	}
142	err = os.RemoveAll(path)
143	if err != nil {
144		return err
145	}
146
147	path = path[:strings.LastIndex(path, "/")]
148	dirs := strings.Split(path, "/")
149
150	for range dirs {
151		if path == "data" {
152			break
153		}
154		err = os.Remove(path)
155		if err != nil {
156			// This folder likely has data, so might as well save some time by
157			// not checking the parents we can't delete anyway.
158			break
159		}
160		path = path[:strings.LastIndex(path, "/")]
161	}
162
163	return nil
164}
165
166// stringifyRepo accepts a repository URI string and the corresponding local
167// filesystem path, whether the URI is HTTP, HTTPS, or SSH.
168func stringifyRepo(url string) (path string, err error) {
169	url = strings.TrimSuffix(url, ".git")
170	url = strings.TrimSuffix(url, "/")
171
172	ep, err := transport.NewEndpoint(url)
173	if err != nil {
174		return "", err
175	}
176
177	if ep.Protocol == "http" || ep.Protocol == "https" {
178		return "data/" + strings.Split(url, "://")[1], nil
179	} else if ep.Protocol == "ssh" {
180		return "data/" + ep.Host + "/" + ep.Path, nil
181	} else {
182		return "", errors.New("unsupported protocol")
183	}
184}