Compare commits
1 Commits
main
...
message-pa
Author | SHA1 | Date |
---|---|---|
Amolith | 8476d3ae6c |
Binary file not shown.
Before Width: | Height: | Size: 209 KiB |
|
@ -1,3 +0,0 @@
|
||||||
SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
|
|
||||||
|
|
||||||
SPDX-License-Identifier: CC0-1.0
|
|
|
@ -5,7 +5,7 @@
|
||||||
/willow
|
/willow
|
||||||
/*.csv
|
/*.csv
|
||||||
/data/
|
/data/
|
||||||
/*.sqlite*
|
/*.sqlite
|
||||||
/config.toml
|
/config.toml
|
||||||
|
|
||||||
/.idea/
|
/.idea/
|
||||||
|
|
140
README.md
140
README.md
|
@ -12,134 +12,74 @@ SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
_Forge-agnostic software release tracker_
|
_Forge-agnostic software release tracker_
|
||||||
|
|
||||||
![screenshot of willow's current web UI](.files/2024-02-24.png)
|
![screenshot of willow's current web UI](.files/2023-10-29.png)
|
||||||
|
|
||||||
_This UI is Amolith's attempt at a balance between simple, pleasant, and
|
_This UI is a minimal proof-of-concept, it's going to change drastically in the
|
||||||
functional. Amolith is not a UX professional and would **very** much welcome
|
near future._
|
||||||
input from someone more knowledgeable!_
|
|
||||||
|
|
||||||
## What is it?
|
## What is it?
|
||||||
|
|
||||||
_If you'd rather watch a short video, Amolith gave a 5-minute [lightning talk on
|
_If you'd rather watch a video, I gave a [lightning talk on Willow] at the 2023
|
||||||
Willow] at the 2023 Ubuntu Summit._
|
Ubuntu Summit._
|
||||||
|
|
||||||
[lightning talk on Willow]: https://youtu.be/XIGxKyekvBQ?t=29900
|
[lightning talk on Willow]: https://youtu.be/XIGxKyekvBQ?t=29900
|
||||||
|
|
||||||
Willow helps developers, sysadmins, and homelabbers keep up with software
|
Willow tracks software releases across arbitrary forge platforms by trying to
|
||||||
releases across arbitrary forge platforms, including full-featured forges like
|
support one of the very few things they all have in common: the VCS. At the
|
||||||
GitHub, GitLab, or [Forgejo] as well as more minimal options like [cgit] or
|
moment, git is the _only_ supported VCS, but I would be interested in adding
|
||||||
[stagit].
|
Pijul, Fossil, Mercurial, etc. You can also track releases using RSS feeds.
|
||||||
|
|
||||||
[Forgejo]: https://forgejo.org/
|
Willow exists because decentralisation can be annoying. One piece of software
|
||||||
[cgit]: https://git.zx2c4.com/cgit/
|
can be found on GitHub, another piece on GitLab, one on Bitbucket, a fourth on
|
||||||
[stagit]: https://codemadness.org/stagit.html
|
SourceHut, and a fifth on the developer's self-hosted Forgejo instance. Forgejo
|
||||||
|
and GitHub have RSS feeds that only notify you of releases. GitLab doesn't
|
||||||
|
support RSS feeds for anything, just an API you can poke. Some software updates
|
||||||
|
might be on the developers' personal blog. Sometimes there are CVEs for specific
|
||||||
|
software and they get published somewhere completely different before they're
|
||||||
|
fixed in a release.
|
||||||
|
|
||||||
It exists because decentralisation, as wonderful as it is, does have some pain
|
I want to bring all that scattered information under one roof so a developer or
|
||||||
points. One piece of software is on GitHub, another piece is on GitLab, one on
|
sysadmin can pop open willow's web UI and immediately see what needs updating
|
||||||
Bitbucket, a fourth on [SourceHut], a fifth on the developer's self-hosted
|
where. I've recorded some of my other ideas and plans in [my wiki].
|
||||||
Forgejo instance.
|
|
||||||
|
|
||||||
[SourceHut]: https://sourcehut.org/
|
[my wiki]: https://wiki.secluded.site/hypha/willow
|
||||||
|
|
||||||
The capabilities of each platform can also differ, further complicating the
|
|
||||||
space. For example, Forgejo and GitHub have APIs and RSS release feeds,
|
|
||||||
SourceHut has an API and RSS feeds that notify you of _all_ activity in the
|
|
||||||
repo, GitLab only has an API, and there's no standard for discovering the
|
|
||||||
capabilities of arbitrary git frontends like [legit].
|
|
||||||
|
|
||||||
[legit]: https://github.com/icyphox/legit
|
|
||||||
|
|
||||||
And _then_ you have different pieces of information in different places; some
|
|
||||||
developers might publish release announcements on their personal blog and some
|
|
||||||
projects might release security advisories on an external platform prior to
|
|
||||||
publishing a release.
|
|
||||||
|
|
||||||
All this important info is scattered all over the internet. Willow brings some
|
|
||||||
order to that chaos by supporting both RSS and one of the _very_ few things all
|
|
||||||
the forges and frontends have in common: their **V**ersion **C**ontrol
|
|
||||||
**S**ystem. At the moment, [Git] is the _only_ supported VCS, but we're
|
|
||||||
definitely interested in adding support for [Pijul], [Fossil], [Mercurial], and
|
|
||||||
potentially others.
|
|
||||||
|
|
||||||
[Git]: https://git-scm.com/
|
|
||||||
[Pijul]: https://pijul.org/
|
|
||||||
[Fossil]: https://www.fossil-scm.org/
|
|
||||||
[Mercurial]: https://www.mercurial-scm.org/
|
|
||||||
|
|
||||||
Amolith (the creator) has recorded some of his other ideas, thoughts, and plans
|
|
||||||
in [his wiki].
|
|
||||||
|
|
||||||
[his wiki]: https://wiki.secluded.site/hypha/willow
|
|
||||||
|
|
||||||
## Installation and use
|
## Installation and use
|
||||||
|
|
||||||
**Disclaimers:**
|
_**Note:** prebuilt binaries will be available after I release [v0.0.1]_
|
||||||
1. Prebuilt binaries will be available with the [v0.0.1] release, greatly
|
|
||||||
simplifying installation.
|
|
||||||
2. We consider the project _alpha-quality_. There will be bugs.
|
|
||||||
3. Amolith has tried to make the web UI accessible, but is unsure of its current
|
|
||||||
usability.
|
|
||||||
4. The app is not localised yet and English is the only available language.
|
|
||||||
5. Help with any/all of the above is most welcome!
|
|
||||||
|
|
||||||
[v0.0.1]: https://todo.sr.ht/~amolith/willow?search=status%3Aopen%20label%3A%22v0.0.1%22
|
[v0.0.1]: https://todo.sr.ht/~amolith/willow?search=status%3Aopen%20label%3A%22v0.0.1%22
|
||||||
[communication platforms]: #contributing
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
This assumes Willow will run on an always-on server, like a VPS.
|
|
||||||
|
|
||||||
* Clone the repo with `git clone https://git.sr.ht/~amolith/willow`
|
|
||||||
* Enter the repo's folder with `cd willow`
|
|
||||||
* Build the binary with `CGO_ENABLED=0 go build -ldflags="-s -w" -o willow
|
|
||||||
./cmd`
|
|
||||||
* Transfer the binary to the server however you like
|
|
||||||
* Execute the binary with `./willow`
|
|
||||||
* Edit the config with `vim config.toml`
|
|
||||||
* Daemonise Willow using systemd or OpenRC or whatever you prefer
|
|
||||||
* Reverse-proxy the web UI (defaults to `localhost:1313`) with Caddy or NGINX or
|
|
||||||
whatever you prefer
|
|
||||||
|
|
||||||
### Use
|
|
||||||
|
|
||||||
|
* Clone the repo
|
||||||
|
* Build the binary with `CGO_ENABLED=0 go build -ldflags="-s -w" -o willow ./cmd`
|
||||||
|
* Upload it to a remote server
|
||||||
|
* Execute the binary
|
||||||
|
* Edit the `config.toml`
|
||||||
* Create a user with `./willow -a <username>`
|
* Create a user with `./willow -a <username>`
|
||||||
* Open the web UI (defaults to `localhost:1313`, but [installation] had you put
|
* Execute the binary again
|
||||||
a proxy in front)
|
* Reverse proxy `http://localhost:1313`
|
||||||
|
* Open the web UI
|
||||||
* Click `Track new project`
|
* Click `Track new project`
|
||||||
* Fill out the form and press `Next`
|
* Fill out the form
|
||||||
* Indicate which version you're currently on and press `Track releases`
|
* Indicate which version you're currently on
|
||||||
* You're now tracking that project's releases!
|
* That's it!
|
||||||
|
|
||||||
[installation]: #installation
|
Note that I still consider the project to be in _alpha_ state. There will be
|
||||||
|
bugs; please help fix them!
|
||||||
If you no longer use that project, click the `Delete?` link to remove it, and,
|
|
||||||
if applicable, Willow's copy of its repo.
|
|
||||||
|
|
||||||
If you're no longer running the version Willow says you've selected, click the
|
|
||||||
`Modify?` link to select a different version.
|
|
||||||
|
|
||||||
If there are projects where your selected version does _not_ match what Willow
|
|
||||||
thinks is latest, they'll show up at the top under the **Outdated projects**
|
|
||||||
heading and have a link at the bottom of the card to `View release notes`.
|
|
||||||
Clicking that link populates the right column with those release notes.
|
|
||||||
|
|
||||||
If there are projects where your selected version _does_ match what Willow
|
|
||||||
thinks is latest, they'll show up at the bottom under the **Up-to-date
|
|
||||||
projects** heading.
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions are very much welcome! Please take a look at the [ticket
|
Contributions are very much welcome! Please take a look at the [ticket
|
||||||
tracker][todo] and see if there's anything you're interested in working on. If
|
tracker][todo] and see if there's anything you're interested in working on. If
|
||||||
there's specific functionality you'd like to see implemented and it's not
|
there's specific functionality you'd like to see implemented and it's not
|
||||||
mentioned in the ticket tracker, please describe it through one of the platforms
|
mentioned in the ticket tracker, please send a description to the [mailing
|
||||||
below so we can discuss its inclusion. If we don't feel like it fits with
|
list][email] so we can discuss its inclusion. If I don't feel like it fits with
|
||||||
Willow's goals, you're encouraged to fork the project and make whatever changes
|
Willow's goals, you're encouraged to fork the project and make whatever changes
|
||||||
you like!
|
you like!
|
||||||
|
|
||||||
Questions, comments, and patches can always go to the [mailing list][email], but
|
Questions, comments, and patches can always be sent to the [mailing
|
||||||
there's also an [IRC channel][irc] and an [XMPP MUC][xmpp] for real-time
|
list][email], but I'm also in the [IRC channel][irc]/[XMPP room][xmpp] pretty
|
||||||
interactions.
|
much 24/7. I might not see messages right away, so please stick around.
|
||||||
|
|
||||||
- Email: [~amolith/willow@lists.sr.ht][email]
|
- Email: [~amolith/willow@lists.sr.ht][email]
|
||||||
- IRC: [irc.libera.chat/#willow][irc]
|
- IRC: [irc.libera.chat/#willow][irc]
|
||||||
|
|
|
@ -22,9 +22,9 @@ func DeleteProject(db *sql.DB, mu *sync.Mutex, id string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProject returns a project from the database
|
// GetProject returns a project from the database
|
||||||
func GetProject(db *sql.DB, id string) (map[string]string, error) {
|
func GetProject(db *sql.DB, url string) (map[string]string, error) {
|
||||||
var name, forge, url, version string
|
var id, name, forge, version string
|
||||||
err := db.QueryRow("SELECT name, forge, url, version FROM projects WHERE id = ?", id).Scan(&name, &forge, &url, &version)
|
err := db.QueryRow("SELECT id, name, forge, version FROM projects WHERE url = ?", url).Scan(&id, &name, &forge, &version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UpsertRelease adds or updates a release for a project with a given ID in the
|
// UpsertRelease adds or updates a release for a project with a given URL in the
|
||||||
// database
|
// database
|
||||||
func UpsertRelease(db *sql.DB, mu *sync.Mutex, id, projectID, url, tag, content, date string) error {
|
func UpsertRelease(db *sql.DB, mu *sync.Mutex, id, projectID, url, tag, content, date string) error {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
|
@ -49,7 +49,6 @@ func GetReleases(db *sql.DB, projectID string) ([]map[string]string, error) {
|
||||||
}
|
}
|
||||||
releases = append(releases, map[string]string{
|
releases = append(releases, map[string]string{
|
||||||
"id": id,
|
"id": id,
|
||||||
"project_id": projectID,
|
|
||||||
"url": url,
|
"url": url,
|
||||||
"tag": tag,
|
"tag": tag,
|
||||||
"content": content,
|
"content": content,
|
||||||
|
|
36
git/git.go
36
git/git.go
|
@ -7,6 +7,7 @@ package git
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -148,23 +149,42 @@ func RemoveRepo(url string) (err error) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
path = path[:strings.LastIndex(path, "/")]
|
// TODO: Check whether the two parent directories are empty and remove them if
|
||||||
dirs := strings.Split(path, "/")
|
// so
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
for range dirs {
|
path = strings.TrimSuffix(path, "/")
|
||||||
if path == "data" {
|
if path == "data" {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
empty, err := dirEmpty(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if empty {
|
||||||
err = os.Remove(path)
|
err = os.Remove(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// This folder likely has data, so might as well save some time by
|
return err
|
||||||
// not checking the parents we can't delete anyway.
|
}
|
||||||
break
|
|
||||||
}
|
}
|
||||||
path = path[:strings.LastIndex(path, "/")]
|
path = path[:strings.LastIndex(path, "/")]
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// stringifyRepo accepts a repository URI string and the corresponding local
|
// stringifyRepo accepts a repository URI string and the corresponding local
|
||||||
|
|
|
@ -7,7 +7,6 @@ package project
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -42,29 +41,20 @@ type Release struct {
|
||||||
|
|
||||||
// GetReleases returns a list of all releases for a project from the database
|
// GetReleases returns a list of all releases for a project from the database
|
||||||
func GetReleases(dbConn *sql.DB, mu *sync.Mutex, proj Project) (Project, error) {
|
func GetReleases(dbConn *sql.DB, mu *sync.Mutex, proj Project) (Project, error) {
|
||||||
proj.ID = GenProjectID(proj.URL, proj.Name, proj.Forge)
|
|
||||||
|
|
||||||
ret, err := db.GetReleases(dbConn, proj.ID)
|
ret, err := db.GetReleases(dbConn, proj.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return proj, err
|
return proj, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: figure out a clean way to remove this so the home page loads
|
||||||
|
// immediately.
|
||||||
if len(ret) == 0 {
|
if len(ret) == 0 {
|
||||||
proj, err = fetchReleases(dbConn, mu, proj)
|
|
||||||
if err != nil {
|
|
||||||
return proj, err
|
|
||||||
}
|
|
||||||
err = upsertReleases(dbConn, mu, proj.ID, proj.Releases)
|
|
||||||
if err != nil {
|
|
||||||
return proj, err
|
|
||||||
}
|
|
||||||
return proj, nil
|
return proj, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, row := range ret {
|
for _, row := range ret {
|
||||||
proj.Releases = append(proj.Releases, Release{
|
proj.Releases = append(proj.Releases, Release{
|
||||||
ID: row["id"],
|
ID: row["id"],
|
||||||
ProjectID: proj.ID,
|
|
||||||
Tag: row["tag"],
|
Tag: row["tag"],
|
||||||
Content: row["content"],
|
Content: row["content"],
|
||||||
URL: row["release_url"],
|
URL: row["release_url"],
|
||||||
|
@ -93,7 +83,7 @@ func fetchReleases(dbConn *sql.DB, mu *sync.Mutex, p Project) (Project, error) {
|
||||||
URL: release.URL,
|
URL: release.URL,
|
||||||
Date: release.Date,
|
Date: release.Date,
|
||||||
})
|
})
|
||||||
err = upsertReleases(dbConn, mu, p.ID, p.Releases)
|
err = upsertRelease(dbConn, mu, p.URL, p.Releases)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error upserting release: %v", err)
|
log.Printf("Error upserting release: %v", err)
|
||||||
return p, err
|
return p, err
|
||||||
|
@ -112,7 +102,7 @@ func fetchReleases(dbConn *sql.DB, mu *sync.Mutex, p Project) (Project, error) {
|
||||||
URL: release.URL,
|
URL: release.URL,
|
||||||
Date: release.Date,
|
Date: release.Date,
|
||||||
})
|
})
|
||||||
err = upsertReleases(dbConn, mu, p.ID, p.Releases)
|
err = upsertRelease(dbConn, mu, p.URL, p.Releases)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error upserting release: %v", err)
|
log.Printf("Error upserting release: %v", err)
|
||||||
return p, err
|
return p, err
|
||||||
|
@ -130,18 +120,12 @@ func SortReleases(releases []Release) []Release {
|
||||||
return releases
|
return releases
|
||||||
}
|
}
|
||||||
|
|
||||||
func SortProjects(projects []Project) []Project {
|
// upsertRelease updates or inserts a release in the database
|
||||||
sort.Slice(projects, func(i, j int) bool {
|
func upsertRelease(dbConn *sql.DB, mu *sync.Mutex, url string, releases []Release) error {
|
||||||
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, mu *sync.Mutex, projID string, releases []Release) error {
|
|
||||||
for _, release := range releases {
|
for _, release := range releases {
|
||||||
date := release.Date.Format("2006-01-02 15:04:05")
|
date := release.Date.Format("2006-01-02 15:04:05")
|
||||||
err := db.UpsertRelease(dbConn, mu, release.ID, projID, release.URL, release.Tag, release.Content, date)
|
id := GenReleaseID(url, release.URL, release.Tag)
|
||||||
|
err := db.UpsertRelease(dbConn, mu, id, url, release.URL, release.Tag, release.Content, date)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error upserting release: %v", err)
|
log.Printf("Error upserting release: %v", err)
|
||||||
return err
|
return err
|
||||||
|
@ -172,19 +156,12 @@ func Track(dbConn *sql.DB, mu *sync.Mutex, manualRefresh *chan struct{}, name, u
|
||||||
}
|
}
|
||||||
|
|
||||||
func Untrack(dbConn *sql.DB, mu *sync.Mutex, id string) {
|
func Untrack(dbConn *sql.DB, mu *sync.Mutex, id string) {
|
||||||
proj, err := db.GetProject(dbConn, id)
|
err := db.DeleteProject(dbConn, mu, id)
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error getting project:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = db.DeleteProject(dbConn, mu, proj["id"])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error deleting project:", err)
|
fmt.Println("Error deleting project:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: before removing, check whether other tracked projects use the same
|
err = git.RemoveRepo(id)
|
||||||
// repo
|
|
||||||
err = git.RemoveRepo(proj["url"])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
}
|
}
|
||||||
|
@ -210,7 +187,7 @@ func RefreshLoop(dbConn *sql.DB, mu *sync.Mutex, interval int, manualRefresh, re
|
||||||
return strings.ToLower(projectsList[i].Name) < strings.ToLower(projectsList[j].Name)
|
return strings.ToLower(projectsList[i].Name) < strings.ToLower(projectsList[j].Name)
|
||||||
})
|
})
|
||||||
for i := range projectsList {
|
for i := range projectsList {
|
||||||
err = upsertReleases(dbConn, mu, projectsList[i].ID, projectsList[i].Releases)
|
err = upsertRelease(dbConn, mu, projectsList[i].URL, projectsList[i].Releases)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error upserting release:", err)
|
fmt.Println("Error upserting release:", err)
|
||||||
continue
|
continue
|
||||||
|
@ -237,26 +214,24 @@ func RefreshLoop(dbConn *sql.DB, mu *sync.Mutex, interval int, manualRefresh, re
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProject returns a project from the database
|
// GetProject returns a project from the database
|
||||||
func GetProject(dbConn *sql.DB, proj Project) (Project, error) {
|
func GetProject(dbConn *sql.DB, id string) (Project, error) {
|
||||||
projectDB, err := db.GetProject(dbConn, proj.ID)
|
projectDB, err := db.GetProject(dbConn, id)
|
||||||
if err != nil && errors.Is(err, sql.ErrNoRows) {
|
if err != nil {
|
||||||
return proj, nil
|
return Project{}, err
|
||||||
} else if err != nil {
|
|
||||||
return proj, err
|
|
||||||
}
|
}
|
||||||
p := Project{
|
p := Project{
|
||||||
ID: proj.ID,
|
ID: projectDB["id"],
|
||||||
URL: proj.URL,
|
URL: projectDB["url"],
|
||||||
Name: proj.Name,
|
Name: projectDB["name"],
|
||||||
Forge: proj.Forge,
|
Forge: projectDB["forge"],
|
||||||
Running: projectDB["version"],
|
Running: projectDB["version"],
|
||||||
}
|
}
|
||||||
return p, err
|
return p, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProjectWithReleases returns a single project from the database along with its releases
|
// GetProjectWithReleases returns a single project from the database along with its releases
|
||||||
func GetProjectWithReleases(dbConn *sql.DB, mu *sync.Mutex, proj Project) (Project, error) {
|
func GetProjectWithReleases(dbConn *sql.DB, mu *sync.Mutex, id string) (Project, error) {
|
||||||
project, err := GetProject(dbConn, proj)
|
project, err := GetProject(dbConn, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Project{}, err
|
return Project{}, err
|
||||||
}
|
}
|
||||||
|
@ -282,7 +257,7 @@ func GetProjects(dbConn *sql.DB) ([]Project, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return SortProjects(projects), nil
|
return projects, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProjectsWithReleases returns a list of all projects and all their releases
|
// GetProjectsWithReleases returns a list of all projects and all their releases
|
||||||
|
@ -301,5 +276,5 @@ func GetProjectsWithReleases(dbConn *sql.DB, mu *sync.Mutex) ([]Project, error)
|
||||||
projects[i].Releases = SortReleases(projects[i].Releases)
|
projects[i].Releases = SortReleases(projects[i].Releases)
|
||||||
}
|
}
|
||||||
|
|
||||||
return SortProjects(projects), nil
|
return projects, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,11 +21,12 @@
|
||||||
<header class="wrapper">
|
<header class="wrapper">
|
||||||
<h1>Willow <span><a href="/logout">Log out</a></span></h1>
|
<h1>Willow <span><a href="/logout">Log out</a></span></h1>
|
||||||
<p><a href="/new">Track a new project</a></p>
|
<p><a href="/new">Track a new project</a></p>
|
||||||
|
<p class="message-{{ .MessageType }}">{{ .Message }}</p>
|
||||||
</header>
|
</header>
|
||||||
<div class="two_column">
|
<div class="two_column">
|
||||||
<div class="projects">
|
<div class="projects">
|
||||||
<!-- Range through projects that aren't yet up-to-date -->
|
<!-- Range through projects that aren't yet up-to-date -->
|
||||||
{{- range . -}}
|
{{- range .Projects -}}
|
||||||
{{- if ne .Running (index .Releases 0).Tag -}}
|
{{- if ne .Running (index .Releases 0).Tag -}}
|
||||||
<h2>Outdated projects</h2>
|
<h2>Outdated projects</h2>
|
||||||
{{- break -}}
|
{{- break -}}
|
||||||
|
@ -34,7 +35,7 @@
|
||||||
{{- range . -}}
|
{{- range . -}}
|
||||||
{{- if ne .Running (index .Releases 0).Tag -}}
|
{{- if ne .Running (index .Releases 0).Tag -}}
|
||||||
<div id="{{ .ID }}" class="project card">
|
<div id="{{ .ID }}" class="project card">
|
||||||
<h3><a href="{{ .URL }}">{{ .Name }}</a> <span class="delete"><a href="/new?action=delete&id={{ .ID }}">Delete?</a></span></h3>
|
<h3><a href="{{ .URL }}">{{ .Name }}</a> <span class="delete"><a href="/new?action=delete&url={{ .URL }}">Delete?</a></span></h3>
|
||||||
<p>You've selected {{ .Running }}. <a href="/new?action=update&url={{ .URL }}&forge={{ .Forge }}&name={{ .Name }}">Modify?</a></p>
|
<p>You've selected {{ .Running }}. <a href="/new?action=update&url={{ .URL }}&forge={{ .Forge }}&name={{ .Name }}">Modify?</a></p>
|
||||||
<p>Latest: <a href="{{ (index .Releases 0).URL }}">{{ (index .Releases 0).Tag }}</a></p>
|
<p>Latest: <a href="{{ (index .Releases 0).URL }}">{{ (index .Releases 0).Tag }}</a></p>
|
||||||
<p><a href="#{{ (index .Releases 0).ID }}">View release notes</a></p>
|
<p><a href="#{{ (index .Releases 0).ID }}">View release notes</a></p>
|
||||||
|
@ -43,16 +44,16 @@
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
<!-- Range through projects that _are_ up-to-date -->
|
<!-- Range through projects that _are_ up-to-date -->
|
||||||
{{- range . -}}
|
{{- range .Projects -}}
|
||||||
{{- if eq .Running (index .Releases 0).Tag -}}
|
{{- if eq .Running (index .Releases 0).Tag -}}
|
||||||
<h2>Up-to-date projects</h2>
|
<h2>Up-to-date projects</h2>
|
||||||
{{- break -}}
|
{{- break -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- range . -}}
|
{{- range .Projects -}}
|
||||||
{{- if eq .Running (index .Releases 0).Tag -}}
|
{{- if eq .Running (index .Releases 0).Tag -}}
|
||||||
<div class="project card">
|
<div class="project card">
|
||||||
<h3><a href="{{ .URL }}">{{ .Name }}</a> <span class="delete"><a href="/new?action=delete&id={{ .ID }}">Delete?</a></span></h3>
|
<h3><a href="{{ .URL }}">{{ .Name }}</a> <span class="delete"><a href="/new?action=delete&url={{ .URL }}">Delete?</a></span></h3>
|
||||||
<p>You've selected <a href="#{{ (index .Releases 0).ID }}">{{ .Running }}</a>. <a href="/new?action=update&url={{ .URL }}&forge={{ .Forge }}&name={{ .Name }}">Modify?</a></p>
|
<p>You've selected <a href="#{{ (index .Releases 0).ID }}">{{ .Running }}</a>. <a href="/new?action=update&url={{ .URL }}&forge={{ .Forge }}&name={{ .Name }}">Modify?</a></p>
|
||||||
</div>
|
</div>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
@ -60,7 +61,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="release_notes">
|
<div class="release_notes">
|
||||||
<h2>Release notes</h2>
|
<h2>Release notes</h2>
|
||||||
{{- range . -}}
|
{{- range .Projects -}}
|
||||||
<div id="{{ (index .Releases 0).ID }}" class="release_note card">
|
<div id="{{ (index .Releases 0).ID }}" class="release_note card">
|
||||||
<h3>{{ .Name }}: release notes for <a href="{{ (index .Releases 0).URL }}">{{ (index .Releases 0).Tag }}</a> <span class="close"><a href="#">✖</a></span></h3>
|
<h3>{{ .Name }}: release notes for <a href="{{ (index .Releases 0).URL }}">{{ (index .Releases 0).Tag }}</a> <span class="close"><a href="#">✖</a></span></h3>
|
||||||
{{- if eq .Forge "github" "gitea" "forgejo" -}}
|
{{- if eq .Forge "github" "gitea" "forgejo" -}}
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
<input type="hidden" name="name" value="{{ .Name }}">
|
<input type="hidden" name="name" value="{{ .Name }}">
|
||||||
<input type="hidden" name="forge" value="{{ .Forge }}">
|
<input type="hidden" name="forge" value="{{ .Forge }}">
|
||||||
<input type="hidden" name="id" value="{{ .ID }}">
|
<input type="hidden" name="id" value="{{ .ID }}">
|
||||||
<input class="button" type="submit" formaction="/new" value="Track releases">
|
<input class="button" type="submit" formaction="/new" value="Track future releases">
|
||||||
</form>
|
</form>
|
||||||
<!-- Append these if they ever start limiting RSS entries: `(eq $forge "gitea") (eq $forge "forgejo")` -->
|
<!-- Append these if they ever start limiting RSS entries: `(eq $forge "gitea") (eq $forge "forgejo")` -->
|
||||||
{{- if or (eq $forge "github") -}}
|
{{- if or (eq $forge "github") -}}
|
||||||
|
|
|
@ -128,7 +128,7 @@ header > h1 > span {
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
border: 2px solid #424242;
|
border: 2px solid #ccc;
|
||||||
background: #1c1c1c;
|
background: #1c1c1c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
38
ws/ws.go
38
ws/ws.go
|
@ -29,6 +29,12 @@ type Handler struct {
|
||||||
Mu *sync.Mutex
|
Mu *sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type page struct {
|
||||||
|
Projects []project.Project
|
||||||
|
Message string
|
||||||
|
MessageType string
|
||||||
|
}
|
||||||
|
|
||||||
//go:embed static
|
//go:embed static
|
||||||
var fs embed.FS
|
var fs embed.FS
|
||||||
|
|
||||||
|
@ -51,7 +57,12 @@ func (h Handler) RootHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tmpl := template.Must(template.ParseFS(fs, "static/home.html"))
|
tmpl := template.Must(template.ParseFS(fs, "static/home.html"))
|
||||||
if err := tmpl.Execute(w, data); err != nil {
|
p := page{
|
||||||
|
Projects: data,
|
||||||
|
Message: "Hello world",
|
||||||
|
MessageType: "info",
|
||||||
|
}
|
||||||
|
if err := tmpl.Execute(w, p); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,6 +91,17 @@ func (h Handler) NewHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
proj, err := project.GetProject(h.DbConn, submittedURL)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
_, err := w.Write([]byte(fmt.Sprintf("Error getting project: %s", err)))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if proj.Running == "" {
|
||||||
forge := bmStrict.Sanitize(params.Get("forge"))
|
forge := bmStrict.Sanitize(params.Get("forge"))
|
||||||
if forge == "" {
|
if forge == "" {
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
@ -100,23 +122,17 @@ func (h Handler) NewHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
proj := project.Project{
|
proj = project.Project{
|
||||||
ID: project.GenProjectID(submittedURL, name, forge),
|
|
||||||
URL: submittedURL,
|
URL: submittedURL,
|
||||||
Name: name,
|
Name: name,
|
||||||
Forge: forge,
|
Forge: forge,
|
||||||
}
|
}
|
||||||
|
|
||||||
proj, err := project.GetProject(h.DbConn, proj)
|
proj.URL = strings.TrimSuffix(proj.URL, ".git")
|
||||||
if err != nil && err != sql.ErrNoRows {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
_, err := w.Write([]byte(fmt.Sprintf("Error getting project: %s", err)))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
proj.ID = project.GenProjectID(proj.URL, proj.Name, proj.Forge)
|
||||||
proj, err = project.GetReleases(h.DbConn, h.Mu, proj)
|
proj, err = project.GetReleases(h.DbConn, h.Mu, proj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
|
Loading…
Reference in New Issue