Something went wrong. Try again.
Forge-agnostic software release tracker (WIP)
Something went wrong. Try again.
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>//// SPDX-License-Identifier: Apache-2.0
package project
import ( "crypto/sha256" "database/sql" "errors" "fmt" "log" "sort" "strings" "sync" "time"
"github.com/unascribed/FlexVer/go/flexver"
"git.sr.ht/~amolith/willow/db" "git.sr.ht/~amolith/willow/git" "git.sr.ht/~amolith/willow/rss")
type Project struct { ID string URL string Name string Forge string Running string Releases []Release}
type Release struct { ID string ProjectID string URL string Tag string Content string Date time.Time}
// GetReleases returns a list of all releases for a project from the database.func GetReleases(dbConn *sql.DB, mutex *sync.Mutex, proj Project) (Project, error) { proj.ID = GenProjectID(proj.URL, proj.Name, proj.Forge)
ret, err := db.GetReleases(dbConn, proj.ID) if err != nil { return proj, err }
if len(ret) == 0 { return fetchReleases(dbConn, mutex, proj) }
for _, row := range ret { proj.Releases = append(proj.Releases, Release{ ID: row["id"], ProjectID: proj.ID, Tag: row["tag"], Content: row["content"], URL: row["release_url"], Date: time.Time{}, }) } proj.Releases = SortReleases(proj.Releases) return proj, nil}
// fetchReleases fetches releases from a project's forge given its URI.func fetchReleases(dbConn *sql.DB, mutex *sync.Mutex, project Project) (Project, error) { var err error switch project.Forge { case "github", "gitea", "forgejo": rssReleases, err := rss.GetReleases(project.URL) if err != nil { fmt.Println("Error getting RSS releases:", err) return project, err } for _, release := range rssReleases { project.Releases = append(project.Releases, Release{ ID: GenReleaseID(project.URL, release.URL, release.Tag), Tag: release.Tag, Content: release.Content, URL: release.URL, Date: release.Date, }) err = upsertReleases(dbConn, mutex, project.ID, project.Releases) if err != nil { log.Printf("Error upserting release: %v", err) return project, err } } default: gitReleases, err := git.GetReleases(project.URL, project.Forge) if err != nil { return project, err } for _, release := range gitReleases { project.Releases = append(project.Releases, Release{ ID: GenReleaseID(project.URL, release.URL, release.Tag), Tag: release.Tag, Content: release.Content, URL: release.URL, Date: release.Date, }) err = upsertReleases(dbConn, mutex, project.ID, project.Releases) if err != nil { log.Printf("Error upserting release: %v", err) return project, err } } } project.Releases = SortReleases(project.Releases) return project, err}
func SortReleases(releases []Release) []Release { sort.Slice(releases, func(i, j int) bool { return !flexver.Less(releases[i].Tag, releases[j].Tag) }) return releases}
func SortProjects(projects []Project) []Project { sort.Slice(projects, func(i, j int) bool { return strings.ToLower(projects[i].Name) < strings.ToLower(projects[j].Name) }) return projects}
// upsertReleases updates or inserts a release in the database.func upsertReleases(dbConn *sql.DB, mutex *sync.Mutex, projID string, releases []Release) error { for _, release := range releases { date := release.Date.Format("2006-01-02 15:04:05") err := db.UpsertRelease(dbConn, mutex, release.ID, projID, release.URL, release.Tag, release.Content, date) if err != nil { log.Printf("Error upserting release: %v", err) return err } } return nil}
// GenReleaseID generates a likely-unique ID from its project's URL, its release's URL, and its tag.func GenReleaseID(projectURL, releaseURL, tag string) string { idByte := sha256.Sum256([]byte(projectURL + releaseURL + tag)) return fmt.Sprintf("%x", idByte)}
// GenProjectID generates a likely-unique ID from a project's URI, name, and forge.func GenProjectID(url, name, forge string) string { idByte := sha256.Sum256([]byte(url + name + forge)) return fmt.Sprintf("%x", idByte)}
func Track(dbConn *sql.DB, mutex *sync.Mutex, manualRefresh *chan struct{}, name, url, forge, release string) { id := GenProjectID(url, name, forge) err := db.UpsertProject(dbConn, mutex, id, url, name, forge, release) if err != nil { fmt.Println("Error upserting project:", err) } *manualRefresh <- struct{}{}}
func Untrack(dbConn *sql.DB, mutex *sync.Mutex, id string) { proj, err := db.GetProject(dbConn, id) if err != nil { fmt.Println("Error getting project:", err) }
err = db.DeleteProject(dbConn, mutex, proj["id"]) if err != nil { fmt.Println("Error deleting project:", err) }
// TODO: before removing, check whether other tracked projects use the same // repo err = git.RemoveRepo(proj["url"]) if err != nil { log.Println(err) }}
func RefreshLoop(dbConn *sql.DB, mutex *sync.Mutex, interval int, manualRefresh, req *chan struct{}, res *chan []Project) { ticker := time.NewTicker(time.Second * time.Duration(interval))
fetch := func() []Project { projectsList, err := GetProjects(dbConn) if err != nil { fmt.Println("Error getting projects:", err) } for idx, project := range projectsList { p, err := fetchReleases(dbConn, mutex, project) if err != nil { fmt.Println(err) continue } projectsList[idx] = p } sort.Slice(projectsList, func(i, j int) bool { return strings.ToLower(projectsList[i].Name) < strings.ToLower(projectsList[j].Name) }) for i := range projectsList { err = upsertReleases(dbConn, mutex, projectsList[i].ID, projectsList[i].Releases) if err != nil { fmt.Println("Error upserting release:", err) continue } } return projectsList }
projects := fetch()
for { select { case <-ticker.C: projects = fetch() case <-*manualRefresh: ticker.Reset(time.Second * 3600) projects = fetch() case <-*req: projectsCopy := make([]Project, len(projects)) copy(projectsCopy, projects) *res <- projectsCopy } }}
// GetProject returns a project from the database.func GetProject(dbConn *sql.DB, proj Project) (Project, error) { projectDB, err := db.GetProject(dbConn, proj.ID) if err != nil && errors.Is(err, sql.ErrNoRows) { return proj, nil } else if err != nil { return proj, err } project := Project{ ID: proj.ID, URL: proj.URL, Name: proj.Name, Forge: proj.Forge, Running: projectDB["version"], } return project, err}
// GetProjectWithReleases returns a single project from the database along with its releases.func GetProjectWithReleases(dbConn *sql.DB, mutex *sync.Mutex, proj Project) (Project, error) { project, err := GetProject(dbConn, proj) if err != nil { return Project{}, err }
return GetReleases(dbConn, mutex, project)}
// GetProjects returns a list of all projects from the database.func GetProjects(dbConn *sql.DB) ([]Project, error) { projectsDB, err := db.GetProjects(dbConn) if err != nil { return nil, err }
projects := make([]Project, len(projectsDB)) for i, project := range projectsDB { projects[i] = Project{ ID: project["id"], URL: project["url"], Name: project["name"], Forge: project["forge"], Running: project["version"], } }
return SortProjects(projects), nil}
// GetProjectsWithReleases returns a list of all projects and all their releases// from the database.func GetProjectsWithReleases(dbConn *sql.DB, mutex *sync.Mutex) ([]Project, error) { projects, err := GetProjects(dbConn) if err != nil { return nil, err }
for i := range projects { projects[i], err = GetReleases(dbConn, mutex, projects[i]) if err != nil { return nil, err } projects[i].Releases = SortReleases(projects[i].Releases) }
return SortProjects(projects), nil}