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