mirror of
https://github.com/Gu1llaum-3/sshm.git
synced 2025-10-19 01:17:20 +02:00
feat: add automatic version update checking and notifications
- Add internal/version module for GitHub release checking - Integrate async version check in Bubble Tea UI - Display update notification in main interface - Add version check to --version/-v command output - Include comprehensive version comparison and error handling - Add unit tests for version parsing and comparison logic
This commit is contained in:
parent
ef075e74cf
commit
5c832ce26f
38
cmd/root.go
38
cmd/root.go
@ -1,19 +1,22 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/Gu1llaum-3/sshm/internal/config"
|
"github.com/Gu1llaum-3/sshm/internal/config"
|
||||||
"github.com/Gu1llaum-3/sshm/internal/ui"
|
"github.com/Gu1llaum-3/sshm/internal/ui"
|
||||||
|
"github.com/Gu1llaum-3/sshm/internal/version"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
// version will be set at build time via -ldflags
|
// appVersion will be set at build time via -ldflags
|
||||||
var version = "dev"
|
var appVersion = "dev"
|
||||||
|
|
||||||
// configFile holds the path to the SSH config file
|
// configFile holds the path to the SSH config file
|
||||||
var configFile string
|
var configFile string
|
||||||
@ -29,7 +32,7 @@ Main usage:
|
|||||||
You can also use sshm in CLI mode for direct operations.
|
You can also use sshm in CLI mode for direct operations.
|
||||||
|
|
||||||
Hosts are read from your ~/.ssh/config file by default.`,
|
Hosts are read from your ~/.ssh/config file by default.`,
|
||||||
Version: version,
|
Version: appVersion,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
// If no arguments provided, run interactive mode
|
// If no arguments provided, run interactive mode
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
@ -85,7 +88,7 @@ func runInteractiveMode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run the interactive TUI
|
// Run the interactive TUI
|
||||||
if err := ui.RunInteractiveMode(hosts, configFile); err != nil {
|
if err := ui.RunInteractiveMode(hosts, configFile, appVersion); err != nil {
|
||||||
log.Fatalf("Error running interactive mode: %v", err)
|
log.Fatalf("Error running interactive mode: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -136,6 +139,30 @@ func connectToHost(hostName string) {
|
|||||||
fmt.Printf("%s\n", strings.Join(sshCmd, " "))
|
fmt.Printf("%s\n", strings.Join(sshCmd, " "))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getVersionWithUpdateCheck returns a custom version string with update check
|
||||||
|
func getVersionWithUpdateCheck() string {
|
||||||
|
versionText := fmt.Sprintf("sshm version %s", appVersion)
|
||||||
|
|
||||||
|
// Check for updates
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
updateInfo, err := version.CheckForUpdates(ctx, appVersion)
|
||||||
|
if err != nil {
|
||||||
|
// Return just version if check fails
|
||||||
|
return versionText + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
if updateInfo != nil && updateInfo.Available {
|
||||||
|
versionText += fmt.Sprintf("\n🚀 Update available: %s → %s (%s)",
|
||||||
|
updateInfo.CurrentVer,
|
||||||
|
updateInfo.LatestVer,
|
||||||
|
updateInfo.ReleaseURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
return versionText + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||||
func Execute() {
|
func Execute() {
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
@ -147,4 +174,7 @@ func Execute() {
|
|||||||
func init() {
|
func init() {
|
||||||
// Add the config file flag
|
// Add the config file flag
|
||||||
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "SSH config file to use (default: ~/.ssh/config)")
|
rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "SSH config file to use (default: ~/.ssh/config)")
|
||||||
|
|
||||||
|
// Set custom version template with update check
|
||||||
|
rootCmd.SetVersionTemplate(getVersionWithUpdateCheck())
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"github.com/Gu1llaum-3/sshm/internal/config"
|
"github.com/Gu1llaum-3/sshm/internal/config"
|
||||||
"github.com/Gu1llaum-3/sshm/internal/connectivity"
|
"github.com/Gu1llaum-3/sshm/internal/connectivity"
|
||||||
"github.com/Gu1llaum-3/sshm/internal/history"
|
"github.com/Gu1llaum-3/sshm/internal/history"
|
||||||
|
"github.com/Gu1llaum-3/sshm/internal/version"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/table"
|
"github.com/charmbracelet/bubbles/table"
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
@ -78,6 +79,10 @@ type Model struct {
|
|||||||
sortMode SortMode
|
sortMode SortMode
|
||||||
configFile string // Path to the SSH config file
|
configFile string // Path to the SSH config file
|
||||||
|
|
||||||
|
// Version update information
|
||||||
|
updateInfo *version.UpdateInfo
|
||||||
|
currentVersion string
|
||||||
|
|
||||||
// View management
|
// View management
|
||||||
viewMode ViewMode
|
viewMode ViewMode
|
||||||
addForm *addFormModel
|
addForm *addFormModel
|
||||||
|
@ -178,12 +178,13 @@ func (m *Model) updateTableHeight() {
|
|||||||
// Calculate dynamic table height based on terminal size
|
// Calculate dynamic table height based on terminal size
|
||||||
// Layout breakdown:
|
// Layout breakdown:
|
||||||
// - ASCII title: 5 lines (1 empty + 4 text lines)
|
// - ASCII title: 5 lines (1 empty + 4 text lines)
|
||||||
|
// - Update banner : 1 line (if present)
|
||||||
// - Search bar: 1 line
|
// - Search bar: 1 line
|
||||||
// - Help text: 1 line
|
// - Help text: 1 line
|
||||||
// - App margins/spacing: 3 lines
|
// - App margins/spacing: 3 lines
|
||||||
// - Safety margin: 3 lines (to ensure UI elements are always visible)
|
// - Safety margin: 3 lines (to ensure UI elements are always visible)
|
||||||
// Total reserved: 13 lines minimum to preserve essential UI elements
|
// Total reserved: 14 lines minimum to preserve essential UI elements
|
||||||
reservedHeight := 13
|
reservedHeight := 14
|
||||||
availableHeight := m.height - reservedHeight
|
availableHeight := m.height - reservedHeight
|
||||||
hostCount := len(m.table.Rows())
|
hostCount := len(m.table.Rows())
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// NewModel creates a new TUI model with the given SSH hosts
|
// NewModel creates a new TUI model with the given SSH hosts
|
||||||
func NewModel(hosts []config.SSHHost, configFile string) Model {
|
func NewModel(hosts []config.SSHHost, configFile, currentVersion string) Model {
|
||||||
// Initialize the history manager
|
// Initialize the history manager
|
||||||
historyManager, err := history.NewHistoryManager()
|
historyManager, err := history.NewHistoryManager()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -38,6 +38,7 @@ func NewModel(hosts []config.SSHHost, configFile string) Model {
|
|||||||
pingManager: pingManager,
|
pingManager: pingManager,
|
||||||
sortMode: SortByName,
|
sortMode: SortByName,
|
||||||
configFile: configFile,
|
configFile: configFile,
|
||||||
|
currentVersion: currentVersion,
|
||||||
styles: styles,
|
styles: styles,
|
||||||
width: 80,
|
width: 80,
|
||||||
height: 24,
|
height: 24,
|
||||||
@ -136,8 +137,8 @@ func NewModel(hosts []config.SSHHost, configFile string) Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RunInteractiveMode starts the interactive TUI interface
|
// RunInteractiveMode starts the interactive TUI interface
|
||||||
func RunInteractiveMode(hosts []config.SSHHost, configFile string) error {
|
func RunInteractiveMode(hosts []config.SSHHost, configFile, currentVersion string) error {
|
||||||
m := NewModel(hosts, configFile)
|
m := NewModel(hosts, configFile, currentVersion)
|
||||||
|
|
||||||
// Start the application in alt screen mode for clean output
|
// Start the application in alt screen mode for clean output
|
||||||
p := tea.NewProgram(m, tea.WithAltScreen())
|
p := tea.NewProgram(m, tea.WithAltScreen())
|
||||||
|
@ -8,14 +8,17 @@ import (
|
|||||||
|
|
||||||
"github.com/Gu1llaum-3/sshm/internal/config"
|
"github.com/Gu1llaum-3/sshm/internal/config"
|
||||||
"github.com/Gu1llaum-3/sshm/internal/connectivity"
|
"github.com/Gu1llaum-3/sshm/internal/connectivity"
|
||||||
|
"github.com/Gu1llaum-3/sshm/internal/version"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Messages for SSH ping functionality
|
// Messages for SSH ping functionality and version checking
|
||||||
type (
|
type (
|
||||||
pingResultMsg *connectivity.HostPingResult
|
pingResultMsg *connectivity.HostPingResult
|
||||||
|
versionCheckMsg *version.UpdateInfo
|
||||||
|
versionErrorMsg error
|
||||||
)
|
)
|
||||||
|
|
||||||
// startPingAllCmd creates a command to ping all hosts concurrently
|
// startPingAllCmd creates a command to ping all hosts concurrently
|
||||||
@ -49,12 +52,33 @@ func pingSingleHostCmd(pingManager *connectivity.PingManager, host config.SSHHos
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkVersionCmd creates a command to check for version updates
|
||||||
|
func checkVersionCmd(currentVersion string) tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
updateInfo, err := version.CheckForUpdates(ctx, currentVersion)
|
||||||
|
if err != nil {
|
||||||
|
return versionErrorMsg(err)
|
||||||
|
}
|
||||||
|
return versionCheckMsg(updateInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Init initializes the model
|
// Init initializes the model
|
||||||
func (m Model) Init() tea.Cmd {
|
func (m Model) Init() tea.Cmd {
|
||||||
return tea.Batch(
|
var cmds []tea.Cmd
|
||||||
textinput.Blink,
|
|
||||||
// Ping is now optional - use 'p' key to start ping
|
// Basic initialization commands
|
||||||
)
|
cmds = append(cmds, textinput.Blink)
|
||||||
|
|
||||||
|
// Check for version updates if we have a current version
|
||||||
|
if m.currentVersion != "" {
|
||||||
|
cmds = append(cmds, checkVersionCmd(m.currentVersion))
|
||||||
|
}
|
||||||
|
|
||||||
|
return tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update handles model updates
|
// Update handles model updates
|
||||||
@ -115,6 +139,19 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
|
case versionCheckMsg:
|
||||||
|
// Handle version check result
|
||||||
|
if msg != nil {
|
||||||
|
m.updateInfo = msg
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case versionErrorMsg:
|
||||||
|
// Handle version check error (silently - not critical)
|
||||||
|
// We don't want to show error messages for version checks
|
||||||
|
// as it might disrupt the user experience
|
||||||
|
return m, nil
|
||||||
|
|
||||||
case addFormSubmitMsg:
|
case addFormSubmitMsg:
|
||||||
if msg.err != nil {
|
if msg.err != nil {
|
||||||
// Show error in form
|
// Show error in form
|
||||||
|
@ -54,6 +54,20 @@ func (m Model) renderListView() string {
|
|||||||
// Add the ASCII title
|
// Add the ASCII title
|
||||||
components = append(components, m.styles.Header.Render(asciiTitle))
|
components = append(components, m.styles.Header.Render(asciiTitle))
|
||||||
|
|
||||||
|
// Add update notification if available (between title and search)
|
||||||
|
if m.updateInfo != nil && m.updateInfo.Available {
|
||||||
|
updateText := fmt.Sprintf("🚀 Update available: %s → %s",
|
||||||
|
m.updateInfo.CurrentVer,
|
||||||
|
m.updateInfo.LatestVer)
|
||||||
|
|
||||||
|
updateStyle := lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("10")). // Green color
|
||||||
|
Bold(true).
|
||||||
|
Align(lipgloss.Center) // Center the notification
|
||||||
|
|
||||||
|
components = append(components, updateStyle.Render(updateText))
|
||||||
|
}
|
||||||
|
|
||||||
// Add the search bar with the appropriate style based on focus
|
// Add the search bar with the appropriate style based on focus
|
||||||
searchPrompt := "Search (/ to focus): "
|
searchPrompt := "Search (/ to focus): "
|
||||||
if m.searchMode {
|
if m.searchMode {
|
||||||
@ -157,3 +171,30 @@ func (m Model) renderDeleteConfirmation() string {
|
|||||||
|
|
||||||
return box.Render(raw)
|
return box.Render(raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// renderUpdateNotification renders the update notification banner
|
||||||
|
func (m Model) renderUpdateNotification() string {
|
||||||
|
if m.updateInfo == nil || !m.updateInfo.Available {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the notification message
|
||||||
|
message := fmt.Sprintf("🚀 Update available: %s → %s",
|
||||||
|
m.updateInfo.CurrentVer,
|
||||||
|
m.updateInfo.LatestVer)
|
||||||
|
|
||||||
|
// Add release URL if available
|
||||||
|
if m.updateInfo.ReleaseURL != "" {
|
||||||
|
message += fmt.Sprintf(" • View release: %s", m.updateInfo.ReleaseURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style the notification with a bright color to make it stand out
|
||||||
|
notificationStyle := lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#00FF00")). // Bright green
|
||||||
|
Bold(true).
|
||||||
|
Padding(0, 1).
|
||||||
|
Border(lipgloss.RoundedBorder()).
|
||||||
|
BorderForeground(lipgloss.Color("#00AA00")) // Darker green border
|
||||||
|
|
||||||
|
return notificationStyle.Render(message)
|
||||||
|
}
|
||||||
|
145
internal/version/version.go
Normal file
145
internal/version/version.go
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GitHubRelease represents a GitHub release response
|
||||||
|
type GitHubRelease struct {
|
||||||
|
TagName string `json:"tag_name"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
HTMLURL string `json:"html_url"`
|
||||||
|
Prerelease bool `json:"prerelease"`
|
||||||
|
Draft bool `json:"draft"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateInfo contains information about available updates
|
||||||
|
type UpdateInfo struct {
|
||||||
|
Available bool
|
||||||
|
CurrentVer string
|
||||||
|
LatestVer string
|
||||||
|
ReleaseURL string
|
||||||
|
ReleaseName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseVersion extracts version numbers from a version string (e.g., "v1.2.3" -> [1, 2, 3])
|
||||||
|
func parseVersion(version string) []int {
|
||||||
|
// Remove 'v' prefix if present
|
||||||
|
version = strings.TrimPrefix(version, "v")
|
||||||
|
|
||||||
|
parts := strings.Split(version, ".")
|
||||||
|
nums := make([]int, len(parts))
|
||||||
|
|
||||||
|
for i, part := range parts {
|
||||||
|
// Remove any non-numeric suffixes (e.g., "1-beta", "2-rc1")
|
||||||
|
numPart := strings.FieldsFunc(part, func(r rune) bool {
|
||||||
|
return r == '-' || r == '+' || r == '_'
|
||||||
|
})[0]
|
||||||
|
|
||||||
|
if num, err := strconv.Atoi(numPart); err == nil {
|
||||||
|
nums[i] = num
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nums
|
||||||
|
}
|
||||||
|
|
||||||
|
// compareVersions compares two version strings
|
||||||
|
// Returns: -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2
|
||||||
|
func compareVersions(v1, v2 string) int {
|
||||||
|
nums1 := parseVersion(v1)
|
||||||
|
nums2 := parseVersion(v2)
|
||||||
|
|
||||||
|
// Pad with zeros to make lengths equal
|
||||||
|
maxLen := len(nums1)
|
||||||
|
if len(nums2) > maxLen {
|
||||||
|
maxLen = len(nums2)
|
||||||
|
}
|
||||||
|
|
||||||
|
for len(nums1) < maxLen {
|
||||||
|
nums1 = append(nums1, 0)
|
||||||
|
}
|
||||||
|
for len(nums2) < maxLen {
|
||||||
|
nums2 = append(nums2, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare each part
|
||||||
|
for i := 0; i < maxLen; i++ {
|
||||||
|
if nums1[i] < nums2[i] {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if nums1[i] > nums2[i] {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckForUpdates checks GitHub for the latest release of sshm
|
||||||
|
func CheckForUpdates(ctx context.Context, currentVersion string) (*UpdateInfo, error) {
|
||||||
|
// Skip version check if current version is "dev"
|
||||||
|
if currentVersion == "dev" {
|
||||||
|
return &UpdateInfo{
|
||||||
|
Available: false,
|
||||||
|
CurrentVer: currentVersion,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create HTTP client with timeout
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create request with context
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET",
|
||||||
|
"https://api.github.com/repos/Gu1llaum-3/sshm/releases/latest", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set user agent
|
||||||
|
req.Header.Set("User-Agent", "sshm/"+currentVersion)
|
||||||
|
|
||||||
|
// Make the request
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch latest release: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("GitHub API returned status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the response
|
||||||
|
var release GitHubRelease
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip pre-releases and drafts
|
||||||
|
if release.Prerelease || release.Draft {
|
||||||
|
return &UpdateInfo{
|
||||||
|
Available: false,
|
||||||
|
CurrentVer: currentVersion,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare versions
|
||||||
|
updateAvailable := compareVersions(currentVersion, release.TagName) < 0
|
||||||
|
|
||||||
|
return &UpdateInfo{
|
||||||
|
Available: updateAvailable,
|
||||||
|
CurrentVer: currentVersion,
|
||||||
|
LatestVer: release.TagName,
|
||||||
|
ReleaseURL: release.HTMLURL,
|
||||||
|
ReleaseName: release.Name,
|
||||||
|
}, nil
|
||||||
|
}
|
56
internal/version/version_test.go
Normal file
56
internal/version/version_test.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseVersion(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
version string
|
||||||
|
expected []int
|
||||||
|
}{
|
||||||
|
{"v1.2.3", []int{1, 2, 3}},
|
||||||
|
{"1.2.3", []int{1, 2, 3}},
|
||||||
|
{"v2.0.0", []int{2, 0, 0}},
|
||||||
|
{"1.2.3-beta", []int{1, 2, 3}},
|
||||||
|
{"1.2.3-rc1", []int{1, 2, 3}},
|
||||||
|
{"dev", []int{0}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
result := parseVersion(test.version)
|
||||||
|
if len(result) != len(test.expected) {
|
||||||
|
t.Errorf("parseVersion(%q) length = %d, want %d", test.version, len(result), len(test.expected))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for i, v := range result {
|
||||||
|
if v != test.expected[i] {
|
||||||
|
t.Errorf("parseVersion(%q)[%d] = %d, want %d", test.version, i, v, test.expected[i])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompareVersions(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
v1 string
|
||||||
|
v2 string
|
||||||
|
expected int
|
||||||
|
}{
|
||||||
|
{"v1.0.0", "v1.0.1", -1},
|
||||||
|
{"v1.0.1", "v1.0.0", 1},
|
||||||
|
{"v1.0.0", "v1.0.0", 0},
|
||||||
|
{"1.2.3", "1.2.4", -1},
|
||||||
|
{"2.0.0", "1.9.9", 1},
|
||||||
|
{"1.2.3-beta", "1.2.3", 0}, // Should ignore suffixes
|
||||||
|
{"1.2.3", "1.2.3-rc1", 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
result := compareVersions(test.v1, test.v2)
|
||||||
|
if result != test.expected {
|
||||||
|
t.Errorf("compareVersions(%q, %q) = %d, want %d", test.v1, test.v2, result, test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user