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/go-git/go-git/v5"
16 "github.com/go-git/go-git/v5/plumbing"
17 "github.com/go-git/go-git/v5/plumbing/transport"
18 "github.com/microcosm-cc/bluemonday"
19)
20
21type Release struct {
22 Tag string
23 Content string
24 URL string
25 Date time.Time
26}
27
28var (
29 bmUGC = bluemonday.UGCPolicy()
30 bmStrict = bluemonday.StrictPolicy()
31)
32
33// listRemoteTags lists all tags in a remote repository, whether HTTP(S) or SSH.
34// func listRemoteTags(url string) (tags []string, err error) {
35// // TODO: Implement listRemoteTags
36// // https://pkg.go.dev/github.com/go-git/go-git/v5@v5.8.0#NewRemote
37// return nil, nil
38// }
39
40// GetReleases fetches all releases in a remote repository, whether HTTP(S) or
41// SSH.
42func GetReleases(gitURI, forge string) ([]Release, error) {
43 r, err := minimalClone(gitURI)
44 if err != nil {
45 return nil, err
46 }
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 (
69 message string
70 date time.Time
71 )
72
73 if errors.Is(err, plumbing.ErrObjectNotFound) {
74 commitTag, err := r.CommitObject(tagRef.Hash())
75 if err != nil {
76 return err
77 }
78
79 message = commitTag.Message
80 date = commitTag.Committer.When
81 } else {
82 message = tagObj.Message
83 date = tagObj.Tagger.When
84 }
85
86 var tagURL string
87
88 tagName := bmStrict.Sanitize(tagRef.Name().Short())
89
90 switch forge {
91 case "sourcehut":
92 tagURL = "https://" + httpURI + "/refs/" + tagName
93 case "gitlab":
94 tagURL = "https://" + httpURI + "/-/releases/" + tagName
95 default:
96 tagURL = ""
97 }
98
99 releases = append(releases, Release{
100 Tag: tagName,
101 Content: bmUGC.Sanitize(message),
102 URL: tagURL,
103 Date: date,
104 })
105
106 return nil
107 })
108 if err != nil {
109 return nil, err
110 }
111
112 return releases, nil
113}
114
115// minimalClone clones a repository with a depth of 1 and no checkout.
116func minimalClone(url string) (r *git.Repository, err error) {
117 path, err := stringifyRepo(url)
118 if err != nil {
119 return nil, err
120 }
121
122 r, err = git.PlainOpen(path)
123 if err == nil {
124 err = r.Fetch(&git.FetchOptions{
125 RemoteName: "origin",
126 RemoteURL: "",
127 RefSpecs: nil,
128 Depth: 1,
129 Auth: nil,
130 Progress: nil,
131 Tags: git.AllTags,
132 Force: false,
133 InsecureSkipTLS: false,
134 CABundle: nil,
135 ProxyOptions: transport.ProxyOptions{
136 URL: "",
137 Username: "",
138 Password: "",
139 },
140 })
141 if errors.Is(err, git.NoErrAlreadyUpToDate) {
142 return r, nil
143 }
144
145 return r, err
146 } else if !errors.Is(err, git.ErrRepositoryNotExists) {
147 return nil, err
148 }
149
150 r, err = git.PlainClone(path, false, &git.CloneOptions{
151 URL: url,
152 Auth: nil,
153 RemoteName: "",
154 ReferenceName: "",
155 SingleBranch: true,
156 NoCheckout: true,
157 Depth: 1,
158 RecurseSubmodules: 0,
159 Progress: nil,
160 Tags: 0,
161 InsecureSkipTLS: false,
162 CABundle: nil,
163 ProxyOptions: transport.ProxyOptions{
164 URL: "",
165 Username: "",
166 Password: "",
167 },
168 Mirror: false,
169 ShallowSubmodules: false,
170 Shared: false,
171 })
172
173 return r, err
174}
175
176// RemoveRepo removes a repository from the local filesystem.
177func RemoveRepo(url string) (err error) {
178 path, err := stringifyRepo(url)
179 if err != nil {
180 return err
181 }
182
183 err = os.RemoveAll(path)
184 if err != nil {
185 return err
186 }
187
188 path = path[:strings.LastIndex(path, "/")]
189 dirs := strings.Split(path, "/")
190
191 for range dirs {
192 if path == "data" {
193 break
194 }
195
196 err = os.Remove(path)
197 if err != nil {
198 // This folder likely has data, so might as well save some time by
199 // not checking the parents we can't delete anyway.
200 break
201 }
202
203 path = path[:strings.LastIndex(path, "/")]
204 }
205
206 return nil
207}
208
209// stringifyRepo accepts a repository URI string and the corresponding local
210// filesystem path, whether the URI is HTTP, HTTPS, or SSH.
211func stringifyRepo(url string) (path string, err error) {
212 url = strings.TrimSuffix(url, ".git")
213 url = strings.TrimSuffix(url, "/")
214
215 ep, err := transport.NewEndpoint(url)
216 if err != nil {
217 return "", err
218 }
219
220 switch ep.Protocol {
221 case "http", "https":
222 return "data/" + strings.Split(url, "://")[1], nil
223 case "ssh":
224 return "data/" + ep.Host + "/" + ep.Path, nil
225 default:
226 return "", errors.New("unsupported protocol")
227 }
228}