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 var tagURL string
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) (*git.Repository, 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 if errors.Is(err, git.NoErrAlreadyUpToDate) {
123 return r, nil
124 }
125 return r, err
126 } else if !errors.Is(err, git.ErrRepositoryNotExists) {
127 return nil, err
128 }
129
130 r, err = git.PlainClone(path, false, &git.CloneOptions{
131 URL: url,
132 SingleBranch: true,
133 NoCheckout: true,
134 Depth: 1,
135 })
136 return r, err
137}
138
139// RemoveRepo removes a repository from the local filesystem.
140func RemoveRepo(url string) error {
141 path, err := stringifyRepo(url)
142 if err != nil {
143 return err
144 }
145 err = os.RemoveAll(path)
146 if err != nil {
147 return err
148 }
149
150 path = path[:strings.LastIndex(path, "/")]
151 dirs := strings.Split(path, "/")
152
153 for range dirs {
154 if path == "data" {
155 break
156 }
157 err = os.Remove(path)
158 if err != nil {
159 // This folder likely has data, so might as well save some time by
160 // not checking the parents we can't delete anyway.
161 break
162 }
163 path = path[:strings.LastIndex(path, "/")]
164 }
165
166 return nil
167}
168
169// stringifyRepo accepts a repository URI string and the corresponding local
170// filesystem path, whether the URI is HTTP, HTTPS, or SSH.
171func stringifyRepo(url string) (string, error) {
172 url = strings.TrimSuffix(url, ".git")
173 url = strings.TrimSuffix(url, "/")
174
175 ep, err := transport.NewEndpoint(url)
176 if err != nil {
177 return "", err
178 }
179
180 switch ep.Protocol {
181 case "http", "https":
182 return "data/" + strings.Split(url, "://")[1], nil
183 case "ssh":
184 return "data/" + ep.Host + "/" + ep.Path, nil
185 default:
186 return "", errors.New("unsupported protocol")
187 }
188}