diff --git a/go.mod b/go.mod index f55cd06ad4774f8ecf0dc11c7f83d34b810a09ef..37b80d530790ecdb490ac7a57105d01015c23836 100644 --- a/go.mod +++ b/go.mod @@ -54,6 +54,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/tidwall/sjson v1.2.5 github.com/zeebo/xxh3 v1.0.2 + golang.org/x/mod v0.30.0 golang.org/x/sync v0.18.0 golang.org/x/text v0.31.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 diff --git a/go.sum b/go.sum index ec8e97d5390f096c2445ae18ff086997942a683a..b06c00560fa87752445d30f8814015f1d86cee31 100644 --- a/go.sum +++ b/go.sum @@ -401,6 +401,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= diff --git a/internal/update/update.go b/internal/update/update.go index c1be482945cdad76688ba5115bd4fb15b45ad150..4080a6a99976eb8ce9953d5d08bf250880853f77 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -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,10 +38,27 @@ func (i Info) IsDevelopment() bool { // Available returns true if there's an update available. // -// Returns true if the current and latest versions are different, treating -// pre-releases as valid latest versions. +// Fork prereleases are treated as latest for their base version. Other +// prereleases follow standard semver comparison. func (i Info) Available() bool { - return i.Current != i.Latest + vCurrent := "v" + i.Current + vLatest := "v" + i.Latest + + if semver.Compare(vLatest, vCurrent) <= 0 { + return false + } + + // 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. @@ -47,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 { @@ -94,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 } diff --git a/internal/update/update_test.go b/internal/update/update_test.go index 5496f70299d17abea2335a48fcfa1c865e7f192e..aa42c882b9beede322931f62c2f8dc8bcecec319 100644 --- a/internal/update/update_test.go +++ b/internal/update/update_test.go @@ -3,6 +3,7 @@ package update import ( "context" "testing" + "time" "github.com/stretchr/testify/require" ) @@ -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 }