diff options
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/version')
| -rw-r--r-- | raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/version/version.go | 268 | ||||
| -rw-r--r-- | raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/version/version_test.go | 362 |
2 files changed, 630 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/version/version.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/version/version.go new file mode 100644 index 0000000..db8636e --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/version/version.go @@ -0,0 +1,268 @@ +package version + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/log" +) + +type VersionInfo struct { + Current string + Latest string + IsGit bool + IsBranch bool + IsTag bool + HasUpdate bool +} + +// VersionFetcher is an interface for fetching version information +type VersionFetcher interface { + GetCurrentVersion(dmsPath string) (string, error) + GetLatestVersion(dmsPath string) (string, error) +} + +// DefaultVersionFetcher is the default implementation that uses git/curl +type DefaultVersionFetcher struct{} + +func (d *DefaultVersionFetcher) GetCurrentVersion(dmsPath string) (string, error) { + if _, err := os.Stat(filepath.Join(dmsPath, ".git")); err == nil { + originalDir, err := os.Getwd() + if err != nil { + return "", err + } + defer func() { + if err := os.Chdir(originalDir); err != nil { + log.Warnf("failed to change back to original directory: %v", err) + } + }() + + if err := os.Chdir(dmsPath); err != nil { + return "", fmt.Errorf("failed to change to DMS directory: %w", err) + } + + tagCmd := exec.Command("git", "describe", "--exact-match", "--tags", "HEAD") + if tagOutput, err := tagCmd.Output(); err == nil { + return strings.TrimSpace(string(tagOutput)), nil + } + + branchCmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD") + if branchOutput, err := branchCmd.Output(); err == nil { + branch := strings.TrimSpace(string(branchOutput)) + revCmd := exec.Command("git", "rev-parse", "--short", "HEAD") + if revOutput, err := revCmd.Output(); err == nil { + rev := strings.TrimSpace(string(revOutput)) + return fmt.Sprintf("%s@%s", branch, rev), nil + } + return branch, nil + } + } + + cmd := exec.Command("dms", "--version") + if output, err := cmd.Output(); err == nil { + return strings.TrimSpace(string(output)), nil + } + + return "unknown", nil +} + +func (d *DefaultVersionFetcher) GetLatestVersion(dmsPath string) (string, error) { + if _, err := os.Stat(filepath.Join(dmsPath, ".git")); err == nil { + originalDir, err := os.Getwd() + if err != nil { + return "", err + } + defer func() { + if err := os.Chdir(originalDir); err != nil { + log.Warnf("failed to change back to original directory: %v", err) + } + }() + + if err := os.Chdir(dmsPath); err != nil { + return "", fmt.Errorf("failed to change to DMS directory: %w", err) + } + + currentRefCmd := exec.Command("git", "symbolic-ref", "-q", "HEAD") + currentRefOutput, _ := currentRefCmd.Output() + onBranch := len(currentRefOutput) > 0 + + if !onBranch { + tagCmd := exec.Command("git", "describe", "--exact-match", "--tags", "HEAD") + if _, err := tagCmd.Output(); err == nil { + // Add timeout to git fetch to prevent hanging + fetchCmd := exec.Command("timeout", "5s", "git", "fetch", "origin", "--tags", "--quiet") + if err := fetchCmd.Run(); err != nil { + log.Debugf("git fetch tags failed (continuing with local tags): %v", err) + } + + latestTagCmd := exec.Command("git", "tag", "-l", "v*", "--sort=-version:refname") + latestTagOutput, err := latestTagCmd.Output() + if err != nil { + return "", fmt.Errorf("failed to get latest tag: %w", err) + } + + tags := strings.Split(strings.TrimSpace(string(latestTagOutput)), "\n") + if len(tags) == 0 || tags[0] == "" { + return "", fmt.Errorf("no version tags found") + } + return tags[0], nil + } + } else { + branchCmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD") + branchOutput, err := branchCmd.Output() + if err != nil { + return "", fmt.Errorf("failed to get current branch: %w", err) + } + currentBranch := strings.TrimSpace(string(branchOutput)) + + // Add timeout to git fetch to prevent hanging + fetchCmd := exec.Command("timeout", "5s", "git", "fetch", "origin", currentBranch, "--quiet") + if err := fetchCmd.Run(); err != nil { + log.Debugf("git fetch branch failed (continuing with local ref): %v", err) + } + + remoteRevCmd := exec.Command("git", "rev-parse", "--short", fmt.Sprintf("origin/%s", currentBranch)) + remoteRevOutput, err := remoteRevCmd.Output() + if err != nil { + return "", fmt.Errorf("failed to get remote revision: %w", err) + } + remoteRev := strings.TrimSpace(string(remoteRevOutput)) + return fmt.Sprintf("%s@%s", currentBranch, remoteRev), nil + } + } + + // Add timeout to prevent hanging when GitHub is down + cmd := exec.Command("curl", "-s", "--max-time", "5", "https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest") + output, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("failed to fetch latest release: %w", err) + } + + var result struct { + TagName string `json:"tag_name"` + } + if err := json.Unmarshal(output, &result); err != nil { + for _, line := range strings.Split(string(output), "\n") { + if strings.Contains(line, "\"tag_name\"") { + parts := strings.Split(line, "\"") + if len(parts) >= 4 { + return parts[3], nil + } + } + } + return "", fmt.Errorf("failed to parse latest version: %w", err) + } + + return result.TagName, nil +} + +// defaultFetcher is used by the public functions +var defaultFetcher VersionFetcher = &DefaultVersionFetcher{} + +func GetCurrentDMSVersion() (string, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("failed to get home directory: %w", err) + } + + dmsPath := filepath.Join(homeDir, ".config", "quickshell", "dms") + if _, err := os.Stat(dmsPath); os.IsNotExist(err) { + return "", fmt.Errorf("DMS not installed") + } + + return defaultFetcher.GetCurrentVersion(dmsPath) +} + +func GetLatestDMSVersion() (string, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("failed to get home directory: %w", err) + } + + dmsPath := filepath.Join(homeDir, ".config", "quickshell", "dms") + return defaultFetcher.GetLatestVersion(dmsPath) +} + +func GetDMSVersionInfo() (*VersionInfo, error) { + return GetDMSVersionInfoWithFetcher(defaultFetcher) +} + +func GetDMSVersionInfoWithFetcher(fetcher VersionFetcher) (*VersionInfo, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("failed to get home directory: %w", err) + } + + dmsPath := filepath.Join(homeDir, ".config", "quickshell", "dms") + if _, err := os.Stat(dmsPath); os.IsNotExist(err) { + return nil, fmt.Errorf("DMS not installed") + } + + current, err := fetcher.GetCurrentVersion(dmsPath) + if err != nil { + return nil, err + } + + latest, err := fetcher.GetLatestVersion(dmsPath) + if err != nil { + return nil, fmt.Errorf("failed to get latest version: %w", err) + } + + info := &VersionInfo{ + Current: current, + Latest: latest, + IsGit: strings.Contains(current, "@"), + IsBranch: strings.Contains(current, "@"), + IsTag: !strings.Contains(current, "@") && strings.HasPrefix(current, "v"), + } + + if info.IsBranch { + parts := strings.Split(current, "@") + latestParts := strings.Split(latest, "@") + if len(parts) == 2 && len(latestParts) == 2 { + info.HasUpdate = parts[1] != latestParts[1] + } + } else if info.IsTag { + info.HasUpdate = current != latest + } else { + info.HasUpdate = false + } + + return info, nil +} + +func CompareVersions(v1, v2 string) int { + v1 = strings.TrimPrefix(v1, "v") + v2 = strings.TrimPrefix(v2, "v") + + parts1 := strings.Split(v1, ".") + parts2 := strings.Split(v2, ".") + + maxLen := len(parts1) + if len(parts2) > maxLen { + maxLen = len(parts2) + } + + for i := 0; i < maxLen; i++ { + var p1, p2 int + if i < len(parts1) { + fmt.Sscanf(parts1[i], "%d", &p1) //nolint:errcheck + } + if i < len(parts2) { + fmt.Sscanf(parts2[i], "%d", &p2) //nolint:errcheck + } + + if p1 < p2 { + return -1 + } + if p1 > p2 { + return 1 + } + } + + return 0 +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/version/version_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/version/version_test.go new file mode 100644 index 0000000..1e424c1 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/version/version_test.go @@ -0,0 +1,362 @@ +package version + +import ( + "os" + "os/exec" + "path/filepath" + "testing" + + mocks_version "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/version" + "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" +) + +func TestCompareVersions(t *testing.T) { + tests := []struct { + v1 string + v2 string + expected int + }{ + {"v0.1.0", "v0.1.0", 0}, + {"v0.1.0", "v0.1.1", -1}, + {"v0.1.1", "v0.1.0", 1}, + {"v0.1.10", "v0.1.2", 1}, + {"v0.2.0", "v0.1.9", 1}, + {"0.1.0", "0.1.0", 0}, + {"1.0.0", "v1.0.0", 0}, + {"v1.2.3", "v1.2.4", -1}, + {"v2.0.0", "v1.9.9", 1}, + } + + for _, tt := range tests { + result := CompareVersions(tt.v1, tt.v2) + if result != tt.expected { + t.Errorf("CompareVersions(%q, %q) = %d; want %d", tt.v1, tt.v2, result, tt.expected) + } + } +} + +func TestGetDMSVersionInfo_Structure(t *testing.T) { + // Create a temp directory with a fake DMS installation + tempDir := t.TempDir() + dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms") + os.MkdirAll(dmsPath, 0o755) + + // Create a .git directory to simulate git installation + os.MkdirAll(filepath.Join(dmsPath, ".git"), 0o755) + + originalHome := os.Getenv("HOME") + defer os.Setenv("HOME", originalHome) + os.Setenv("HOME", tempDir) + + // Create mock fetcher + mockFetcher := mocks_version.NewMockVersionFetcher(t) + mockFetcher.EXPECT().GetCurrentVersion(dmsPath).Return("v0.1.0", nil) + mockFetcher.EXPECT().GetLatestVersion(dmsPath).Return("v0.1.1", nil) + + info, err := GetDMSVersionInfoWithFetcher(mockFetcher) + if err != nil { + t.Fatalf("GetDMSVersionInfoWithFetcher() failed: %v", err) + } + + if info == nil { + t.Fatal("GetDMSVersionInfoWithFetcher() returned nil") + } + + if info.Current != "v0.1.0" { + t.Errorf("Current version = %s, expected v0.1.0", info.Current) + } + + if info.Latest != "v0.1.1" { + t.Errorf("Latest version = %s, expected v0.1.1", info.Latest) + } + + if !info.HasUpdate { + t.Error("HasUpdate should be true when current != latest") + } + + if !info.IsTag { + t.Error("IsTag should be true for v0.1.0") + } + + t.Logf("Current: %s, Latest: %s, HasUpdate: %v", info.Current, info.Latest, info.HasUpdate) +} + +func TestGetDMSVersionInfo_BranchVersion(t *testing.T) { + tempDir := t.TempDir() + dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms") + os.MkdirAll(dmsPath, 0o755) + os.MkdirAll(filepath.Join(dmsPath, ".git"), 0o755) + + originalHome := os.Getenv("HOME") + defer os.Setenv("HOME", originalHome) + os.Setenv("HOME", tempDir) + + mockFetcher := mocks_version.NewMockVersionFetcher(t) + mockFetcher.EXPECT().GetCurrentVersion(dmsPath).Return("master@abc1234", nil) + mockFetcher.EXPECT().GetLatestVersion(dmsPath).Return("master@def5678", nil) + + info, err := GetDMSVersionInfoWithFetcher(mockFetcher) + if err != nil { + t.Fatalf("GetDMSVersionInfoWithFetcher() failed: %v", err) + } + + if !info.IsBranch { + t.Error("IsBranch should be true for branch@commit format") + } + + if !info.IsGit { + t.Error("IsGit should be true for branch@commit format") + } + + if !info.HasUpdate { + t.Error("HasUpdate should be true when commits differ") + } +} + +func TestGetDMSVersionInfo_NoUpdate(t *testing.T) { + tempDir := t.TempDir() + dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms") + os.MkdirAll(dmsPath, 0o755) + os.MkdirAll(filepath.Join(dmsPath, ".git"), 0o755) + + originalHome := os.Getenv("HOME") + defer os.Setenv("HOME", originalHome) + os.Setenv("HOME", tempDir) + + mockFetcher := mocks_version.NewMockVersionFetcher(t) + mockFetcher.EXPECT().GetCurrentVersion(dmsPath).Return("v0.1.0", nil) + mockFetcher.EXPECT().GetLatestVersion(dmsPath).Return("v0.1.0", nil) + + info, err := GetDMSVersionInfoWithFetcher(mockFetcher) + if err != nil { + t.Fatalf("GetDMSVersionInfoWithFetcher() failed: %v", err) + } + + if info.HasUpdate { + t.Error("HasUpdate should be false when current == latest") + } +} + +func TestGetCurrentDMSVersion_NotInstalled(t *testing.T) { + originalHome := os.Getenv("HOME") + defer os.Setenv("HOME", originalHome) + + tempDir := t.TempDir() + os.Setenv("HOME", tempDir) + + _, err := GetCurrentDMSVersion() + if err == nil { + t.Error("Expected error when DMS not installed, got nil") + } +} + +func TestGetCurrentDMSVersion_GitTag(t *testing.T) { + if !utils.CommandExists("git") { + t.Skip("git not available") + } + + tempDir := t.TempDir() + dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms") + os.MkdirAll(dmsPath, 0o755) + + originalHome := os.Getenv("HOME") + defer os.Setenv("HOME", originalHome) + os.Setenv("HOME", tempDir) + + exec.Command("git", "init", dmsPath).Run() + exec.Command("git", "-C", dmsPath, "config", "user.email", "test@test.com").Run() + exec.Command("git", "-C", dmsPath, "config", "user.name", "Test User").Run() + + testFile := filepath.Join(dmsPath, "test.txt") + os.WriteFile(testFile, []byte("test"), 0o644) + exec.Command("git", "-C", dmsPath, "add", ".").Run() + exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run() + exec.Command("git", "-C", dmsPath, "tag", "v0.1.0").Run() + + version, err := GetCurrentDMSVersion() + if err != nil { + t.Fatalf("GetCurrentDMSVersion() failed: %v", err) + } + + if version != "v0.1.0" { + t.Errorf("Expected version v0.1.0, got %s", version) + } +} + +func TestGetCurrentDMSVersion_GitBranch(t *testing.T) { + if !utils.CommandExists("git") { + t.Skip("git not available") + } + + tempDir := t.TempDir() + dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms") + os.MkdirAll(dmsPath, 0o755) + + originalHome := os.Getenv("HOME") + defer os.Setenv("HOME", originalHome) + os.Setenv("HOME", tempDir) + + exec.Command("git", "init", dmsPath).Run() + exec.Command("git", "-C", dmsPath, "config", "user.email", "test@test.com").Run() + exec.Command("git", "-C", dmsPath, "config", "user.name", "Test User").Run() + exec.Command("git", "-C", dmsPath, "checkout", "-b", "master").Run() + + testFile := filepath.Join(dmsPath, "test.txt") + os.WriteFile(testFile, []byte("test"), 0o644) + exec.Command("git", "-C", dmsPath, "add", ".").Run() + exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run() + + version, err := GetCurrentDMSVersion() + if err != nil { + t.Fatalf("GetCurrentDMSVersion() failed: %v", err) + } + + if version == "" { + t.Error("Expected non-empty version") + } + + if len(version) < 7 { + t.Errorf("Expected version with branch@commit format, got %s", version) + } +} + +func TestVersionInfo_IsGit(t *testing.T) { + tests := []struct { + current string + isGit bool + isBranch bool + isTag bool + }{ + {"v0.1.0", false, false, true}, + {"master@abc1234", true, true, false}, + {"dev@def5678", true, true, false}, + {"v0.2.0", false, false, true}, + {"unknown", false, false, false}, + } + + for _, tt := range tests { + info := &VersionInfo{ + IsGit: tt.isGit, + IsBranch: tt.isBranch, + IsTag: tt.isTag, + } + + actualIsGit := len(tt.current) > 0 && tt.current[0] != 'v' && tt.current != "unknown" + actualIsBranch := len(tt.current) > 0 && tt.current[0] != 'v' + actualIsTag := len(tt.current) > 0 && tt.current[0] == 'v' + + if tt.current == "unknown" { + actualIsGit = false + actualIsBranch = false + actualIsTag = false + } + + if info.IsGit != tt.isGit { + t.Errorf("For %s: IsGit = %v; want %v", tt.current, info.IsGit, tt.isGit) + } + if info.IsBranch != tt.isBranch { + t.Errorf("For %s: IsBranch = %v; want %v", tt.current, info.IsBranch, tt.isBranch) + } + if info.IsTag != tt.isTag { + t.Errorf("For %s: IsTag = %v; want %v", tt.current, info.IsTag, tt.isTag) + } + + _ = actualIsGit + _ = actualIsBranch + _ = actualIsTag + } +} + +func TestVersionInfo_HasUpdate_Branch(t *testing.T) { + tests := []struct { + current string + latest string + hasUpdate bool + }{ + {"master@abc1234", "master@abc1234", false}, + {"master@abc1234", "master@def5678", true}, + {"dev@abc1234", "dev@abc1234", false}, + {"dev@old1234", "dev@new5678", true}, + } + + for _, tt := range tests { + info := &VersionInfo{ + HasUpdate: tt.hasUpdate, + } + + if info.HasUpdate != tt.hasUpdate { + t.Errorf("For current=%s, latest=%s: HasUpdate = %v; want %v", + tt.current, tt.latest, info.HasUpdate, tt.hasUpdate) + } + } +} + +func TestVersionInfo_HasUpdate_Tag(t *testing.T) { + tests := []struct { + current string + latest string + hasUpdate bool + }{ + {"v0.1.0", "v0.1.0", false}, + {"v0.1.0", "v0.1.1", true}, + {"v0.1.5", "v0.1.5", false}, + {"v0.1.9", "v0.2.0", true}, + } + + for _, tt := range tests { + info := &VersionInfo{ + HasUpdate: tt.hasUpdate, + } + + if info.HasUpdate != tt.hasUpdate { + t.Errorf("For current=%s, latest=%s: HasUpdate = %v; want %v", + tt.current, tt.latest, info.HasUpdate, tt.hasUpdate) + } + } +} + +func TestGetLatestDMSVersion_FallbackParsing(t *testing.T) { + jsonResponse := `{ + "tag_name": "v0.1.17", + "name": "Release v0.1.17" + }` + + lines := []string{ + ` "tag_name": "v0.1.17",`, + ` "name": "Release v0.1.17"`, + } + + for _, line := range lines { + if len(line) > 0 && line[0:15] == ` "tag_name": "` { + parts := []string{"", "", "", "v0.1.17"} + version := parts[3] + if version != "v0.1.17" { + t.Errorf("Failed to parse version from line: %s", line) + } + } + } + + _ = jsonResponse +} + +func TestCompareVersions_EdgeCases(t *testing.T) { + tests := []struct { + v1 string + v2 string + expected int + }{ + {"", "", 0}, + {"v1", "v1", 0}, + {"v1.0", "v1", 0}, + {"v1.0.0", "v1.0", 0}, + {"v1.0.1", "v1.0", 1}, + {"v1", "v1.0.1", -1}, + } + + for _, tt := range tests { + result := CompareVersions(tt.v1, tt.v2) + if result != tt.expected { + t.Errorf("CompareVersions(%q, %q) = %d; want %d", tt.v1, tt.v2, result, tt.expected) + } + } +} |