git.go
1 // SPDX-FileCopyrightText: Amolith <amolith@secluded.site> 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package git 6 7 import ( 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 22 type Release struct { 23 Tag string 24 Content string 25 URL string 26 Date time.Time 27 } 28 29 var ( 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. 43 func 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. 109 func 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 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. 140 func RemoveRepo(url string) (err 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. 171 func stringifyRepo(url string) (path string, err 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 if ep.Protocol == "http" || ep.Protocol == "https" { 181 return "data/" + strings.Split(url, "://")[1], nil 182 } else if ep.Protocol == "ssh" { 183 return "data/" + ep.Host + "/" + ep.Path, nil 184 } else { 185 return "", errors.New("unsupported protocol") 186 } 187 }