2023-09-24 20:57:56 +00:00
|
|
|
// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
|
|
|
|
//
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
2023-10-25 04:14:32 +00:00
|
|
|
package git
|
2023-09-21 18:03:21 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
2023-10-25 04:14:32 +00:00
|
|
|
"fmt"
|
2023-10-29 14:41:00 +00:00
|
|
|
"io"
|
2023-10-25 04:14:32 +00:00
|
|
|
"net/url"
|
2023-09-21 18:03:21 +00:00
|
|
|
"os"
|
|
|
|
"strings"
|
2023-10-25 04:14:32 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/microcosm-cc/bluemonday"
|
2023-09-21 18:03:21 +00:00
|
|
|
|
|
|
|
"github.com/go-git/go-git/v5"
|
|
|
|
"github.com/go-git/go-git/v5/plumbing"
|
|
|
|
"github.com/go-git/go-git/v5/plumbing/transport"
|
|
|
|
)
|
|
|
|
|
2023-10-25 04:14:32 +00:00
|
|
|
type Release struct {
|
|
|
|
Tag string
|
|
|
|
Content string
|
|
|
|
URL string
|
|
|
|
Date time.Time
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
bmUGC = bluemonday.UGCPolicy()
|
|
|
|
bmStrict = bluemonday.StrictPolicy()
|
|
|
|
)
|
|
|
|
|
2023-09-21 18:03:21 +00:00
|
|
|
// listRemoteTags lists all tags in a remote repository, whether HTTP(S) or SSH.
|
2023-09-24 21:02:00 +00:00
|
|
|
// func listRemoteTags(url string) (tags []string, err error) {
|
|
|
|
// // TODO: Implement listRemoteTags
|
|
|
|
// // https://pkg.go.dev/github.com/go-git/go-git/v5@v5.8.0#NewRemote
|
|
|
|
// return nil, nil
|
|
|
|
// }
|
2023-09-21 18:03:21 +00:00
|
|
|
|
2023-10-25 04:14:32 +00:00
|
|
|
// GetReleases fetches all releases in a remote repository, whether HTTP(S) or
|
|
|
|
// SSH.
|
|
|
|
func GetReleases(gitURI, forge string) ([]Release, error) {
|
|
|
|
r, err := minimalClone(gitURI)
|
2023-09-21 18:03:21 +00:00
|
|
|
if err != nil {
|
2023-10-25 04:14:32 +00:00
|
|
|
return nil, err
|
2023-09-21 18:03:21 +00:00
|
|
|
}
|
|
|
|
tagRefs, err := r.Tags()
|
|
|
|
if err != nil {
|
2023-10-25 04:14:32 +00:00
|
|
|
return nil, err
|
2023-09-21 18:03:21 +00:00
|
|
|
}
|
2023-10-25 04:14:32 +00:00
|
|
|
|
|
|
|
parsedURI, err := url.Parse(gitURI)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Println("Error parsing URI: " + err.Error())
|
|
|
|
}
|
|
|
|
|
|
|
|
var httpURI string
|
|
|
|
if parsedURI.Scheme != "" {
|
|
|
|
httpURI = parsedURI.Host + parsedURI.Path
|
|
|
|
}
|
|
|
|
|
|
|
|
releases := make([]Release, 0)
|
|
|
|
|
2023-09-21 18:03:21 +00:00
|
|
|
err = tagRefs.ForEach(func(tagRef *plumbing.Reference) error {
|
2023-10-29 14:41:00 +00:00
|
|
|
tagObj, err := r.TagObject(tagRef.Hash())
|
|
|
|
|
|
|
|
var message string
|
|
|
|
var date time.Time
|
|
|
|
if errors.Is(err, plumbing.ErrObjectNotFound) {
|
|
|
|
commitTag, err := r.CommitObject(tagRef.Hash())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2023-09-21 18:03:21 +00:00
|
|
|
}
|
2023-10-29 14:41:00 +00:00
|
|
|
message = commitTag.Message
|
|
|
|
date = commitTag.Committer.When
|
|
|
|
} else {
|
|
|
|
message = tagObj.Message
|
|
|
|
date = tagObj.Tagger.When
|
|
|
|
}
|
|
|
|
|
|
|
|
tagURL := ""
|
|
|
|
tagName := bmStrict.Sanitize(tagRef.Name().Short())
|
|
|
|
switch forge {
|
|
|
|
case "sourcehut":
|
|
|
|
tagURL = "https://" + httpURI + "/refs/" + tagName
|
|
|
|
case "gitlab":
|
|
|
|
tagURL = "https://" + httpURI + "/-/releases/" + tagName
|
2023-09-21 18:03:21 +00:00
|
|
|
default:
|
2023-10-29 14:41:00 +00:00
|
|
|
tagURL = ""
|
2023-09-21 18:03:21 +00:00
|
|
|
}
|
2023-10-29 14:41:00 +00:00
|
|
|
|
|
|
|
releases = append(releases, Release{
|
|
|
|
Tag: tagName,
|
|
|
|
Content: bmUGC.Sanitize(message),
|
|
|
|
URL: tagURL,
|
|
|
|
Date: date,
|
|
|
|
})
|
2023-09-21 18:03:21 +00:00
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
2023-10-25 04:14:32 +00:00
|
|
|
return nil, err
|
2023-09-21 18:03:21 +00:00
|
|
|
}
|
|
|
|
|
2023-10-25 04:14:32 +00:00
|
|
|
return releases, nil
|
2023-09-21 18:03:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// minimalClone clones a repository with a depth of 1 and no checkout.
|
|
|
|
func minimalClone(url string) (r *git.Repository, err error) {
|
|
|
|
path, err := stringifyRepo(url)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err := os.Stat(path); err == nil {
|
|
|
|
r, err := git.PlainOpen(path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
err = r.Fetch(&git.FetchOptions{
|
|
|
|
RemoteName: "origin",
|
|
|
|
Depth: 1,
|
|
|
|
Tags: git.AllTags,
|
|
|
|
})
|
2023-10-25 04:14:32 +00:00
|
|
|
if errors.Is(err, git.NoErrAlreadyUpToDate) {
|
2023-09-21 18:03:21 +00:00
|
|
|
return r, nil
|
|
|
|
}
|
|
|
|
return r, err
|
|
|
|
}
|
|
|
|
|
|
|
|
r, err = git.PlainClone(path, false, &git.CloneOptions{
|
|
|
|
URL: url,
|
|
|
|
SingleBranch: true,
|
|
|
|
NoCheckout: true,
|
|
|
|
Depth: 1,
|
|
|
|
})
|
|
|
|
return r, err
|
|
|
|
}
|
|
|
|
|
2023-10-25 04:14:32 +00:00
|
|
|
// RemoveRepo removes a repository from the local filesystem.
|
|
|
|
func RemoveRepo(url string) (err error) {
|
2023-09-21 18:03:21 +00:00
|
|
|
path, err := stringifyRepo(url)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
err = os.RemoveAll(path)
|
2023-10-29 14:41:00 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Check whether the two parent directories are empty and remove them if
|
|
|
|
// so
|
|
|
|
for i := 0; i < 2; i++ {
|
|
|
|
path = strings.TrimSuffix(path, "/")
|
|
|
|
if path == "data" {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
empty, err := dirEmpty(path)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if empty {
|
|
|
|
err = os.Remove(path)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
path = path[:strings.LastIndex(path, "/")]
|
|
|
|
}
|
|
|
|
|
2023-09-21 18:03:21 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-10-29 14:41:00 +00:00
|
|
|
// dirEmpty checks if a directory is empty.
|
|
|
|
func dirEmpty(name string) (empty bool, err error) {
|
|
|
|
f, err := os.Open(name)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
_, err = f.Readdirnames(1)
|
|
|
|
if err == io.EOF {
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
2023-09-21 18:03:21 +00:00
|
|
|
// stringifyRepo accepts a repository URI string and the corresponding local
|
|
|
|
// filesystem path, whether the URI is HTTP, HTTPS, or SSH.
|
|
|
|
func stringifyRepo(url string) (path string, err error) {
|
|
|
|
ep, err := transport.NewEndpoint(url)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
if ep.Protocol == "http" || ep.Protocol == "https" {
|
|
|
|
return "data/" + strings.Split(url, "://")[1], nil
|
|
|
|
} else if ep.Protocol == "ssh" {
|
|
|
|
return "data/" + ep.Host + ep.Path, nil
|
|
|
|
} else {
|
|
|
|
return "", errors.New("unsupported protocol")
|
|
|
|
}
|
|
|
|
}
|