package cmd import ( "fmt" "os" "os/exec" "regexp" "runtime/debug" "sort" "strings" "git.db.org.ai/dborg/internal/utils" "github.com/spf13/cobra" ) func getLatestVersion() (string, error) { cmd := exec.Command("git", "ls-remote", "--tags", "--refs", "--sort=-v:refname", utils.RepositoryURL) output, err := cmd.Output() if err != nil { return "", err } lines := strings.Split(strings.TrimSpace(string(output)), "\n") if len(lines) == 0 { return "", fmt.Errorf("no tags found") } parts := strings.Split(lines[0], "refs/tags/") if len(parts) < 2 { return "", fmt.Errorf("invalid tag format") } return strings.TrimSpace(parts[1]), nil } func getAllTags() ([]string, error) { cmd := exec.Command("git", "ls-remote", "--tags", "--refs", utils.RepositoryURL) output, err := cmd.Output() if err != nil { return nil, err } var tags []string lines := strings.Split(strings.TrimSpace(string(output)), "\n") for _, line := range lines { if line == "" { continue } parts := strings.Split(line, "refs/tags/") if len(parts) < 2 { continue } tag := strings.TrimSpace(parts[1]) tags = append(tags, tag) } sort.Slice(tags, func(i, j int) bool { return tags[i] > tags[j] }) return tags, nil } type Commit struct { Type string Scope string Description string Hash string } func getCommitsBetweenVersions(fromVersion, toVersion string) ([]Commit, error) { var commits []Commit fromTag := "" if fromVersion != "" { fromTag = fmt.Sprintf("v%s", strings.TrimPrefix(fromVersion, "v")) } toTag := fmt.Sprintf("v%s", strings.TrimPrefix(toVersion, "v")) var rangeSpec string if fromTag == "" { rangeSpec = toTag } else { rangeSpec = fmt.Sprintf("%s..%s", fromTag, toTag) } cmd := exec.Command("git", "log", "--oneline", "--no-merges", rangeSpec, "--", utils.RepositoryURL) output, err := cmd.Output() if err != nil { return nil, fmt.Errorf("failed to fetch commits: %w", err) } lines := strings.Split(strings.TrimSpace(string(output)), "\n") for _, line := range lines { if line == "" { continue } parts := strings.SplitN(line, " ", 2) if len(parts) < 2 { continue } hash := parts[0] message := parts[1] commit := parseCommitMessage(hash, message) commits = append(commits, commit) } return commits, nil } func parseCommitMessage(hash, message string) Commit { re := regexp.MustCompile(`^(\w+)(?:\(([^)]+)\))?:\s*(.+)$`) matches := re.FindStringSubmatch(message) if len(matches) == 4 { return Commit{ Type: matches[1], Scope: matches[2], Description: matches[3], Hash: hash, } } return Commit{ Type: "chore", Description: message, Hash: hash, } } func displayCommits(commits []Commit) { if len(commits) == 0 { fmt.Println("No changes found.") return } categories := make(map[string][]Commit) for _, commit := range commits { categories[commit.Type] = append(categories[commit.Type], commit) } typeOrder := []string{"feat", "fix", "perf", "refactor", "docs", "style", "test", "chore", "build", "ci"} fmt.Println("\nšŸ“‹ What's new:") fmt.Println(strings.Repeat("═", 50)) for _, commitType := range typeOrder { if commits, exists := categories[commitType]; exists { displayCategory(commitType, commits) delete(categories, commitType) } } for commitType, commits := range categories { displayCategory(commitType, commits) } fmt.Println(strings.Repeat("═", 50)) } func displayCategory(commitType string, commits []Commit) { var icon string switch commitType { case "feat": icon = "✨" case "fix": icon = "šŸ›" case "perf": icon = "⚔" case "refactor": icon = "ā™»ļø" case "docs": icon = "šŸ“" case "style": icon = "šŸ’„" case "test": icon = "āœ…" case "chore": icon = "šŸ”§" case "build": icon = "šŸ“¦" case "ci": icon = "šŸ¤–" default: icon = "šŸ“Œ" } fmt.Printf("\n%s %s\n", icon, strings.ToUpper(commitType)) for _, commit := range commits { if commit.Scope != "" { fmt.Printf(" • %s (%s): %s\n", commit.Description, commit.Scope, commit.Hash[:7]) } else { fmt.Printf(" • %s (%s)\n", commit.Description, commit.Hash[:7]) } } } var updateCmd = &cobra.Command{ Use: "update", Short: "Update dborg to the latest version", Long: `Update dborg by running go install git.db.org.ai/dborg@`, Run: func(cmd *cobra.Command, args []string) { fmt.Println("Checking for updates...") latestVersion, err := getLatestVersion() if err != nil { fmt.Fprintf(os.Stderr, "Failed to get latest version: %v\n", err) os.Exit(1) } currentVersion := "dev" if info, ok := debug.ReadBuildInfo(); ok { if info.Main.Version != "" && info.Main.Version != "(devel)" { currentVersion = info.Main.Version } } if currentVersion != "dev" && currentVersion == latestVersion { fmt.Printf("You're already on the latest version: %s\n", latestVersion) return } fmt.Printf("Updating from %s to %s...\n", currentVersion, latestVersion) commits, err := getCommitsBetweenVersions(currentVersion, latestVersion) if err != nil { fmt.Fprintf(os.Stderr, "Warning: Could not fetch commit changes: %v\n", err) } else { displayCommits(commits) } fmt.Printf("\nšŸš€ Installing dborg %s...\n", latestVersion) installTarget := fmt.Sprintf("git.db.org.ai/dborg@%s", latestVersion) installCmd := exec.Command("go", "install", installTarget) installCmd.Stdout = os.Stdout installCmd.Stderr = os.Stderr if err := installCmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "Failed to update dborg: %v\n", err) os.Exit(1) } fmt.Printf("\nāœ… dborg updated successfully to %s!\n", latestVersion) }, } func init() { rootCmd.AddCommand(updateCmd) }