|
6 | 6 | "os" |
7 | 7 | "os/exec" |
8 | 8 | "regexp" |
| 9 | + "sort" |
| 10 | + "strconv" |
9 | 11 | "strings" |
10 | 12 | "time" |
11 | 13 |
|
@@ -645,33 +647,183 @@ type pollAttemptMsg struct { |
645 | 647 | attempt int |
646 | 648 | } |
647 | 649 |
|
648 | | -func main() { |
649 | | - if len(os.Args) < 2 { |
650 | | - fmt.Println("Error: No tag specified.") |
651 | | - fmt.Println("Usage: tag-release-tui vX.Y.Z [--remote <remote-name>] [--test]") |
652 | | - fmt.Println(" --remote: Specify git remote name (default: origin)") |
653 | | - fmt.Println(" --test: Run in test mode (validation only, no actual changes)") |
654 | | - os.Exit(1) |
| 650 | +// Semantic version parsing and incrementing functions |
| 651 | + |
| 652 | +type semVersion struct { |
| 653 | + major, minor, patch int |
| 654 | + prefix string // v prefix if present |
| 655 | +} |
| 656 | + |
| 657 | +func parseSemanticVersion(version string) (*semVersion, error) { |
| 658 | + // Handle v prefix |
| 659 | + prefix := "" |
| 660 | + if strings.HasPrefix(version, "v") { |
| 661 | + prefix = "v" |
| 662 | + version = version[1:] |
| 663 | + } |
| 664 | + |
| 665 | + parts := strings.Split(version, ".") |
| 666 | + if len(parts) != 3 { |
| 667 | + return nil, fmt.Errorf("invalid semantic version format: %s", version) |
| 668 | + } |
| 669 | + |
| 670 | + major, err := strconv.Atoi(parts[0]) |
| 671 | + if err != nil { |
| 672 | + return nil, fmt.Errorf("invalid major version: %s", parts[0]) |
| 673 | + } |
| 674 | + |
| 675 | + minor, err := strconv.Atoi(parts[1]) |
| 676 | + if err != nil { |
| 677 | + return nil, fmt.Errorf("invalid minor version: %s", parts[1]) |
| 678 | + } |
| 679 | + |
| 680 | + patch, err := strconv.Atoi(parts[2]) |
| 681 | + if err != nil { |
| 682 | + return nil, fmt.Errorf("invalid patch version: %s", parts[2]) |
655 | 683 | } |
656 | 684 |
|
657 | | - tag := os.Args[1] |
| 685 | + return &semVersion{ |
| 686 | + major: major, |
| 687 | + minor: minor, |
| 688 | + patch: patch, |
| 689 | + prefix: prefix, |
| 690 | + }, nil |
| 691 | +} |
| 692 | + |
| 693 | +func (v *semVersion) incrementMinor() *semVersion { |
| 694 | + return &semVersion{ |
| 695 | + major: v.major, |
| 696 | + minor: v.minor + 1, |
| 697 | + patch: 0, // reset patch to 0 when incrementing minor |
| 698 | + prefix: v.prefix, |
| 699 | + } |
| 700 | +} |
| 701 | + |
| 702 | +func (v *semVersion) toString() string { |
| 703 | + return fmt.Sprintf("%s%d.%d.%d", v.prefix, v.major, v.minor, v.patch) |
| 704 | +} |
| 705 | + |
| 706 | +func getNextVersion(remote string) (string, error) { |
| 707 | + // Get all tags from the remote |
| 708 | + cmd := exec.Command("git", "ls-remote", "--tags", remote) |
| 709 | + output, err := cmd.Output() |
| 710 | + if err != nil { |
| 711 | + return "", fmt.Errorf("failed to list remote tags: %v", err) |
| 712 | + } |
| 713 | + |
| 714 | + var versions []*semVersion |
| 715 | + lines := strings.Split(strings.TrimSpace(string(output)), "\n") |
| 716 | + |
| 717 | + for _, line := range lines { |
| 718 | + if line == "" { |
| 719 | + continue |
| 720 | + } |
| 721 | + |
| 722 | + // Parse line format: hash refs/tags/vX.Y.Z |
| 723 | + parts := strings.Fields(line) |
| 724 | + if len(parts) < 2 { |
| 725 | + continue |
| 726 | + } |
| 727 | + |
| 728 | + ref := parts[1] |
| 729 | + if !strings.HasPrefix(ref, "refs/tags/") { |
| 730 | + continue |
| 731 | + } |
| 732 | + |
| 733 | + tag := strings.TrimPrefix(ref, "refs/tags/") |
| 734 | + |
| 735 | + // Skip annotated tag refs (ending with ^{}) |
| 736 | + if strings.HasSuffix(tag, "^{}") { |
| 737 | + continue |
| 738 | + } |
| 739 | + |
| 740 | + // Try to parse as semantic version |
| 741 | + version, err := parseSemanticVersion(tag) |
| 742 | + if err != nil { |
| 743 | + // Skip non-semantic version tags |
| 744 | + continue |
| 745 | + } |
| 746 | + |
| 747 | + versions = append(versions, version) |
| 748 | + } |
| 749 | + |
| 750 | + // If no versions found, start at v0.1.0 |
| 751 | + if len(versions) == 0 { |
| 752 | + return "v0.1.0", nil |
| 753 | + } |
| 754 | + |
| 755 | + // Sort versions to find the latest |
| 756 | + sort.Slice(versions, func(i, j int) bool { |
| 757 | + a, b := versions[i], versions[j] |
| 758 | + if a.major != b.major { |
| 759 | + return a.major < b.major |
| 760 | + } |
| 761 | + if a.minor != b.minor { |
| 762 | + return a.minor < b.minor |
| 763 | + } |
| 764 | + return a.patch < b.patch |
| 765 | + }) |
| 766 | + |
| 767 | + // Get latest version and increment minor |
| 768 | + latest := versions[len(versions)-1] |
| 769 | + next := latest.incrementMinor() |
| 770 | + |
| 771 | + return next.toString(), nil |
| 772 | +} |
| 773 | + |
| 774 | +func main() { |
| 775 | + var tag string |
658 | 776 | testMode := false |
659 | 777 | remote := "origin" // default remote |
660 | 778 |
|
661 | | - // Parse flags |
662 | | - for i := 2; i < len(os.Args); i++ { |
663 | | - switch os.Args[i] { |
664 | | - case "--test", "-t": |
665 | | - testMode = true |
666 | | - case "--remote", "-r": |
667 | | - if i+1 < len(os.Args) { |
668 | | - remote = os.Args[i+1] |
669 | | - i++ // skip next arg |
670 | | - } else { |
671 | | - fmt.Println("Error: --remote flag requires a value") |
672 | | - os.Exit(1) |
| 779 | + // Check if tag is provided as first argument |
| 780 | + if len(os.Args) >= 2 && !strings.HasPrefix(os.Args[1], "--") { |
| 781 | + tag = os.Args[1] |
| 782 | + // Parse remaining flags starting from index 2 |
| 783 | + for i := 2; i < len(os.Args); i++ { |
| 784 | + switch os.Args[i] { |
| 785 | + case "--test", "-t": |
| 786 | + testMode = true |
| 787 | + case "--remote", "-r": |
| 788 | + if i+1 < len(os.Args) { |
| 789 | + remote = os.Args[i+1] |
| 790 | + i++ // skip next arg |
| 791 | + } else { |
| 792 | + fmt.Println("Error: --remote flag requires a value") |
| 793 | + os.Exit(1) |
| 794 | + } |
| 795 | + } |
| 796 | + } |
| 797 | + } else { |
| 798 | + // No tag provided, parse flags first |
| 799 | + for i := 1; i < len(os.Args); i++ { |
| 800 | + switch os.Args[i] { |
| 801 | + case "--test", "-t": |
| 802 | + testMode = true |
| 803 | + case "--remote", "-r": |
| 804 | + if i+1 < len(os.Args) { |
| 805 | + remote = os.Args[i+1] |
| 806 | + i++ // skip next arg |
| 807 | + } else { |
| 808 | + fmt.Println("Error: --remote flag requires a value") |
| 809 | + os.Exit(1) |
| 810 | + } |
673 | 811 | } |
674 | 812 | } |
| 813 | + |
| 814 | + // Auto-generate tag from latest release |
| 815 | + fmt.Printf("No version specified. Determining next version from remote '%s'...\n", remote) |
| 816 | + var err error |
| 817 | + tag, err = getNextVersion(remote) |
| 818 | + if err != nil { |
| 819 | + fmt.Printf("Error determining next version: %v\n", err) |
| 820 | + fmt.Println("\nUsage: tag-release-tui [vX.Y.Z] [--remote <remote-name>] [--test]") |
| 821 | + fmt.Println(" vX.Y.Z: Version tag (if not provided, auto-increments from latest)") |
| 822 | + fmt.Println(" --remote: Specify git remote name (default: origin)") |
| 823 | + fmt.Println(" --test: Run in test mode (validation only, no actual changes)") |
| 824 | + os.Exit(1) |
| 825 | + } |
| 826 | + fmt.Printf("Next version determined: %s\n", tag) |
675 | 827 | } |
676 | 828 |
|
677 | 829 | if testMode { |
|
0 commit comments