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 "io"
11 "net/url"
12 "os"
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 tagObj, err := r.TagObject(tagRef.Hash())
68
69 var message string
70 var date time.Time
71 if errors.Is(err, plumbing.ErrObjectNotFound) {
72 commitTag, err := r.CommitObject(tagRef.Hash())
73 if err != nil {
74 return err
75 }
76 message = commitTag.Message
77 date = commitTag.Committer.When
78 } else {
79 message = tagObj.Message
80 date = tagObj.Tagger.When
81 }
82
83 tagURL := ""
84 tagName := bmStrict.Sanitize(tagRef.Name().Short())
85 switch forge {
86 case "sourcehut":
87 tagURL = "https://" + httpURI + "/refs/" + tagName
88 case "gitlab":
89 tagURL = "https://" + httpURI + "/-/releases/" + tagName
90 default:
91 tagURL = ""
92 }
93
94 releases = append(releases, Release{
95 Tag: tagName,
96 Content: bmUGC.Sanitize(message),
97 URL: tagURL,
98 Date: date,
99 })
100 return nil
101 })
102 if err != nil {
103 return nil, err
104 }
105
106 return releases, nil
107}
108
109// minimalClone clones a repository with a depth of 1 and no checkout.
110func minimalClone(url string) (r *git.Repository, err error) {
111 path, err := stringifyRepo(url)
112 if err != nil {
113 return nil, err
114 }
115
116 if _, err := os.Stat(path); err == nil {
117 r, err := git.PlainOpen(path)
118 if err != nil {
119 return nil, err
120 }
121 err = r.Fetch(&git.FetchOptions{
122 RemoteName: "origin",
123 Depth: 1,
124 Tags: git.AllTags,
125 })
126 if errors.Is(err, git.NoErrAlreadyUpToDate) {
127 return r, nil
128 }
129 return r, err
130 }
131
132 r, err = git.PlainClone(path, false, &git.CloneOptions{
133 URL: url,
134 SingleBranch: true,
135 NoCheckout: true,
136 Depth: 1,
137 })
138 return r, err
139}
140
141// RemoveRepo removes a repository from the local filesystem.
142func RemoveRepo(url string) (err error) {
143 path, err := stringifyRepo(url)
144 if err != nil {
145 return err
146 }
147 err = os.RemoveAll(path)
148 if err != nil {
149 return err
150 }
151
152 // TODO: Check whether the two parent directories are empty and remove them if
153 // so
154 for i := 0; i < 2; i++ {
155 path = strings.TrimSuffix(path, "/")
156 if path == "data" {
157 break
158 }
159 empty, err := dirEmpty(path)
160 if err != nil {
161 return err
162 }
163 if empty {
164 err = os.Remove(path)
165 if err != nil {
166 return err
167 }
168 }
169 path = path[:strings.LastIndex(path, "/")]
170 }
171
172 return err
173}
174
175// dirEmpty checks if a directory is empty.
176func dirEmpty(name string) (empty bool, err error) {
177 f, err := os.Open(name)
178 if err != nil {
179 return false, err
180 }
181 defer f.Close()
182
183 _, err = f.Readdirnames(1)
184 if err == io.EOF {
185 return true, nil
186 }
187 return false, err
188}
189
190// stringifyRepo accepts a repository URI string and the corresponding local
191// filesystem path, whether the URI is HTTP, HTTPS, or SSH.
192func stringifyRepo(url string) (path string, err error) {
193 url = strings.TrimSuffix(url, ".git")
194 url = strings.TrimSuffix(url, "/")
195
196 ep, err := transport.NewEndpoint(url)
197 if err != nil {
198 return "", err
199 }
200
201 if ep.Protocol == "http" || ep.Protocol == "https" {
202 return "data/" + strings.Split(url, "://")[1], nil
203 } else if ep.Protocol == "ssh" {
204 return "data/" + ep.Host + "/" + ep.Path, nil
205 } else {
206 return "", errors.New("unsupported protocol")
207 }
208}