this repo has no description

semver-parse: add bump subcommand

+108 -14
+108 -14
semver-parse/main.go
··· 2 2 3 3 import ( 4 4 "bufio" 5 + "context" 5 6 "flag" 6 7 "fmt" 7 8 "io" 8 9 "os" 10 + "regexp" 11 + "strconv" 9 12 "strings" 10 13 11 14 "github.com/Masterminds/semver/v3" 15 + "github.com/peterbourgon/ff/v3/ffcli" 12 16 ) 13 17 14 18 func main() { 15 19 if err := realMain( 20 + context.Background(), 16 21 os.Args, 17 22 os.Stdin, 18 23 os.Stdout, 19 24 os.Stderr, 20 25 ); err != nil { 21 26 fmt.Fprintln(os.Stderr, err) 27 + os.Exit(1) 22 28 } 23 29 } 24 30 25 31 func realMain( 26 - _ []string, 32 + ctx context.Context, 33 + args []string, 27 34 stdin io.Reader, 28 35 stdout io.Writer, 29 36 stderr io.Writer, 30 37 ) error { 31 - fs := flag.NewFlagSet("semver", flag.ExitOnError) 38 + exec := args[0] 39 + 40 + fs := flag.NewFlagSet(exec, flag.ExitOnError) 32 41 flagCompact := fs.Bool("c", false, "compact output") 33 42 34 - if err := fs.Parse(os.Args[1:]); err != nil { 35 - return err 43 + bumpCmd := &ffcli.Command{ 44 + Name: "bump", 45 + ShortUsage: fmt.Sprintf("%v bump <patch|minor|major|prerelease>", exec), 46 + ShortHelp: "Bump version by incrementing patch, minor, major, or prerelease", 47 + Exec: func(ctx context.Context, args []string) error { 48 + if len(args) != 1 { 49 + return fmt.Errorf("bump requires exactly one argument: patch, minor, major, or prerelease") 50 + } 51 + 52 + bumpType := args[0] 53 + if bumpType != "patch" && bumpType != "minor" && bumpType != "major" && bumpType != "prerelease" { 54 + return fmt.Errorf("invalid bump type: %s (must be patch, minor, major, or prerelease)", bumpType) 55 + } 56 + 57 + scanner := bufio.NewScanner(stdin) 58 + for scanner.Scan() { 59 + line := scanner.Text() 60 + version, err := semver.NewVersion(line) 61 + if err != nil { 62 + fmt.Fprintf(stderr, "%v: %v\n", err, line) 63 + continue 64 + } 65 + 66 + var newVersion semver.Version 67 + switch bumpType { 68 + case "patch": 69 + newVersion = version.IncPatch() 70 + case "minor": 71 + newVersion = version.IncMinor() 72 + case "major": 73 + newVersion = version.IncMajor() 74 + case "prerelease": 75 + bumped, err := bumpPrerelease(version) 76 + if err != nil { 77 + fmt.Fprintf(stderr, "%v: %v\n", err, line) 78 + continue 79 + } 80 + newVersion = *bumped 81 + } 82 + 83 + fmt.Fprintln(stdout, newVersion.String()) 84 + } 85 + 86 + return scanner.Err() 87 + }, 36 88 } 37 89 38 - scanner := bufio.NewScanner(stdin) 39 - for scanner.Scan() { 40 - line := scanner.Text() 41 - version, err := semver.NewVersion(line) 42 - if err != nil { 43 - fmt.Fprintf(stderr, "%v: %v\n", err, line) 44 - continue 45 - } 90 + rootCmd := &ffcli.Command{ 91 + Name: exec, 92 + ShortUsage: fmt.Sprintf("%v [flags] [<subcommand>]", exec), 93 + FlagSet: fs, 94 + Subcommands: []*ffcli.Command{bumpCmd}, 95 + Exec: func(ctx context.Context, args []string) error { 96 + scanner := bufio.NewScanner(stdin) 97 + for scanner.Scan() { 98 + line := scanner.Text() 99 + version, err := semver.NewVersion(line) 100 + if err != nil { 101 + fmt.Fprintf(stderr, "%v: %v\n", err, line) 102 + continue 103 + } 104 + 105 + dump(stdout, version, *flagCompact) 106 + } 107 + 108 + return scanner.Err() 109 + }, 110 + } 111 + 112 + return rootCmd.ParseAndRun(ctx, args[1:]) 113 + } 114 + 115 + func bumpPrerelease(v *semver.Version) (*semver.Version, error) { 116 + prerelease := v.Prerelease() 117 + 118 + if prerelease == "" && v.Metadata() == "" { 119 + // If no prerelease and no metadata, start a new prerelease cycle. 120 + // Increment patch and add -alpha.0. 121 + newVer := v.IncPatch() 122 + withPre, _ := newVer.SetPrerelease("alpha.0") 123 + return &withPre, nil 124 + } 46 125 47 - dump(stdout, version, *flagCompact) 126 + if prerelease == "" { 127 + return nil, fmt.Errorf("version has metadata but no prerelease to bump") 48 128 } 49 129 50 - return scanner.Err() 130 + re := regexp.MustCompile(`^(alpha|beta|rc)(\.)?(\d+)$`) 131 + if matches := re.FindStringSubmatch(prerelease); matches != nil { 132 + prefix := matches[1] 133 + separator := matches[2] // Will be "." or empty 134 + num, _ := strconv.Atoi(matches[3]) 135 + newPre := fmt.Sprintf("%s%s%d", prefix, separator, num+1) 136 + newVer, _ := semver.NewVersion(fmt.Sprintf("%d.%d.%d-%s", v.Major(), v.Minor(), v.Patch(), newPre)) 137 + if v.Metadata() != "" { 138 + withMeta, _ := newVer.SetMetadata(v.Metadata()) 139 + newVer = &withMeta 140 + } 141 + return newVer, nil 142 + } 143 + 144 + return nil, fmt.Errorf("prerelease '%s' does not match convention (alpha.N, beta.N, rc.N, alphaN, betaN, or rcN)", prerelease) 51 145 } 52 146 53 147 func dump(w io.Writer, v *semver.Version, compact bool) {