@@ -9,15 +9,17 @@ import (
"regexp"
"strings"
"time"
+
+ "golang.org/x/mod/semver"
)
const (
- githubApiUrl = "https://api.github.com/repos/charmbracelet/crush/releases/latest"
- userAgent = "crush/1.0"
+ moduleProxyURL = "https://proxy.golang.org/git.secluded.site/crush/@latest"
+ userAgent = "crush/1.0"
)
// Default is the default [Client].
-var Default Client = &github{}
+var Default Client = &moduleProxy{}
// Info contains information about an available update.
type Info struct {
@@ -36,22 +38,27 @@ func (i Info) IsDevelopment() bool {
// Available returns true if there's an update available.
//
-// If both current and latest are stable versions, returns true if versions are
-// different.
-// If current is a pre-release and latest isn't, returns true.
-// If latest is a pre-release and current isn't, returns false.
+// Fork prereleases are treated as latest for their base version. Other
+// prereleases follow standard semver comparison.
func (i Info) Available() bool {
- cpr := strings.Contains(i.Current, "-")
- lpr := strings.Contains(i.Latest, "-")
- // current is pre release && latest isn't a prerelease
- if cpr && !lpr {
- return true
- }
- // latest is pre release && current isn't a prerelease
- if lpr && !cpr {
+ vCurrent := "v" + i.Current
+ vLatest := "v" + i.Latest
+
+ if semver.Compare(vLatest, vCurrent) <= 0 {
return false
}
- return i.Current != i.Latest
+
+ // Fork vs stable override:
+ // If current is a fork prerelease (contains "-fork") and latest is the
+ // stable version of the same base, don't upgrade.
+ if strings.Contains(i.Current, "-fork") && !strings.Contains(i.Latest, "-") {
+ baseCurrent := strings.Split(vCurrent, "-")[0]
+ if baseCurrent == vLatest {
+ return false
+ }
+ }
+
+ return true
}
// Check checks if a new version is available.
@@ -59,44 +66,43 @@ func Check(ctx context.Context, current string, client Client) (Info, error) {
info := Info{
Current: current,
Latest: current,
+ URL: "https://git.secluded.site/crush",
}
- release, err := client.Latest(ctx)
+ version, err := client.Latest(ctx)
if err != nil {
- return info, fmt.Errorf("failed to fetch latest release: %w", err)
+ return info, fmt.Errorf("failed to fetch latest version: %w", err)
}
- info.Latest = strings.TrimPrefix(release.TagName, "v")
+ info.Latest = strings.TrimPrefix(version.Version, "v")
info.Current = strings.TrimPrefix(info.Current, "v")
- info.URL = release.HTMLURL
return info, nil
}
-// Release represents a GitHub release.
-type Release struct {
- TagName string `json:"tag_name"`
- HTMLURL string `json:"html_url"`
+// ModuleVersion represents a version from the Go module proxy.
+type ModuleVersion struct {
+ Version string `json:"Version"`
+ Time time.Time `json:"Time"`
}
-// Client is a client that can get the latest release.
+// Client is a client that can get the latest version.
type Client interface {
- Latest(ctx context.Context) (*Release, error)
+ Latest(ctx context.Context) (*ModuleVersion, error)
}
-type github struct{}
+type moduleProxy struct{}
// Latest implements [Client].
-func (c *github) Latest(ctx context.Context) (*Release, error) {
+func (c *moduleProxy) Latest(ctx context.Context) (*ModuleVersion, error) {
client := &http.Client{
Timeout: 30 * time.Second,
}
- req, err := http.NewRequestWithContext(ctx, "GET", githubApiUrl, nil)
+ req, err := http.NewRequestWithContext(ctx, "GET", moduleProxyURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", userAgent)
- req.Header.Set("Accept", "application/vnd.github.v3+json")
resp, err := client.Do(req)
if err != nil {
@@ -106,13 +112,13 @@ func (c *github) Latest(ctx context.Context) (*Release, error) {
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
- return nil, fmt.Errorf("github api returned status %d: %s", resp.StatusCode, string(body))
+ return nil, fmt.Errorf("module proxy returned status %d: %s", resp.StatusCode, string(body))
}
- var release Release
- if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
+ var version ModuleVersion
+ if err := json.NewDecoder(resp.Body).Decode(&version); err != nil {
return nil, err
}
- return &release, nil
+ return &version, nil
}
@@ -3,6 +3,7 @@ package update
import (
"context"
"testing"
+ "time"
"github.com/stretchr/testify/require"
)
@@ -19,7 +20,7 @@ func TestCheckForUpdate_Beta(t *testing.T) {
info, err := Check(t.Context(), "v0.10.0", testClient{"v0.11.0-beta.1"})
require.NoError(t, err)
require.NotNil(t, info)
- require.False(t, info.Available())
+ require.True(t, info.Available())
})
t.Run("current is also beta", func(t *testing.T) {
@@ -37,12 +38,35 @@ func TestCheckForUpdate_Beta(t *testing.T) {
})
}
-type testClient struct{ tag string }
+func TestCheckForUpdate_Fork(t *testing.T) {
+ t.Run("fork version vs stable", func(t *testing.T) {
+ info, err := Check(t.Context(), "v0.18.5-fork.1", testClient{"v0.18.5"})
+ require.NoError(t, err)
+ require.NotNil(t, info)
+ require.False(t, info.Available())
+ })
+
+ t.Run("fork version vs newer stable", func(t *testing.T) {
+ info, err := Check(t.Context(), "v0.18.5-fork.1", testClient{"v0.18.6"})
+ require.NoError(t, err)
+ require.NotNil(t, info)
+ require.True(t, info.Available())
+ })
+
+ t.Run("fork version vs newer fork version", func(t *testing.T) {
+ info, err := Check(t.Context(), "v0.18.5-fork.1", testClient{"v0.18.5-fork.2"})
+ require.NoError(t, err)
+ require.NotNil(t, info)
+ require.True(t, info.Available())
+ })
+}
+
+type testClient struct{ version string }
// Latest implements Client.
-func (t testClient) Latest(ctx context.Context) (*Release, error) {
- return &Release{
- TagName: t.tag,
- HTMLURL: "https://example.org",
+func (t testClient) Latest(ctx context.Context) (*ModuleVersion, error) {
+ return &ModuleVersion{
+ Version: t.version,
+ Time: time.Now(),
}, nil
}