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