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}