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 tagName := bmStrict.Sanitize(tagRef.Name().Short())
88
89 switch forge {
90 case "sourcehut":
91 tagURL = "https://" + httpURI + "/refs/" + tagName
92 case "gitlab":
93 tagURL = "https://" + httpURI + "/-/releases/" + tagName
94 default:
95 tagURL = ""
96 }
97
98 releases = append(releases, Release{
99 Tag: tagName,
100 Content: bmUGC.Sanitize(message),
101 URL: tagURL,
102 Date: date,
103 })
104
105 return nil
106 })
107 if err != nil {
108 return nil, err
109 }
110
111 return releases, nil
112}
113
114// minimalClone clones a repository with a depth of 1 and no checkout.
115func minimalClone(url string) (r *git.Repository, err error) {
116 path, err := stringifyRepo(url)
117 if err != nil {
118 return nil, err
119 }
120
121 r, err = git.PlainOpen(path)
122 if err == nil {
123 err = r.Fetch(&git.FetchOptions{
124 RemoteName: "origin",
125 Depth: 1,
126 Tags: git.AllTags,
127 })
128 if errors.Is(err, git.NoErrAlreadyUpToDate) {
129 return r, nil
130 }
131
132 return r, err
133 } else if !errors.Is(err, git.ErrRepositoryNotExists) {
134 return nil, err
135 }
136
137 r, err = git.PlainClone(path, false, &git.CloneOptions{
138 URL: url,
139 SingleBranch: true,
140 NoCheckout: true,
141 Depth: 1,
142 })
143
144 return r, err
145}
146
147// RemoveRepo removes a repository from the local filesystem.
148func RemoveRepo(url string) (err error) {
149 path, err := stringifyRepo(url)
150 if err != nil {
151 return err
152 }
153
154 err = os.RemoveAll(path)
155 if err != nil {
156 return err
157 }
158
159 path = path[:strings.LastIndex(path, "/")]
160 dirs := strings.Split(path, "/")
161
162 for range dirs {
163 if path == "data" {
164 break
165 }
166
167 err = os.Remove(path)
168 if err != nil {
169 // This folder likely has data, so might as well save some time by
170 // not checking the parents we can't delete anyway.
171 break
172 }
173
174 path = path[:strings.LastIndex(path, "/")]
175 }
176
177 return nil
178}
179
180// stringifyRepo accepts a repository URI string and the corresponding local
181// filesystem path, whether the URI is HTTP, HTTPS, or SSH.
182func stringifyRepo(url string) (path string, err error) {
183 url = strings.TrimSuffix(url, ".git")
184 url = strings.TrimSuffix(url, "/")
185
186 ep, err := transport.NewEndpoint(url)
187 if err != nil {
188 return "", err
189 }
190
191 switch ep.Protocol {
192 case "http", "https":
193 return "data/" + strings.Split(url, "://")[1], nil
194 case "ssh":
195 return "data/" + ep.Host + "/" + ep.Path, nil
196 default:
197 return "", errors.New("unsupported protocol")
198 }
199}