// go bump ver tool package main import ( "bufio" "errors" "fmt" "io" "log" "os" "slices" "sort" "strconv" "strings" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/Masterminds/semver/v3" ) var ( cmdExamples = ` **stdin** ❯ echo "1.1.1\n1.2.1" | bumpver patch - 1.2.2 **file** ❯ echo v0.1.0 > VERSION ❯ bumpver patch VERSION 0.1.1 **a minor** ❯ bumpver minor <(git tag -l) 3.4.0 **a major** ❯ bumpver major <(git tag -l) 4.0.0 **create new prerelease** ❯ echo "0.1.0" | bumpver patch --prerelease --prerelease-fmt 'rc.$BumpInt' - 0.1.1-rc.0 ` ) // FmtPart format keywords type FmtPart string var ( // BumpInt an int to be bumped BumpInt FmtPart = "$BumpInt" // KeyArg an keyword argument to the format KeyArg FmtPart = "$KeyArg" ) type preReleaseVersion struct { Fmt []string KeyArgs map[string]string v *semver.Version versionPart string } // incPreRelease bumps a prerelease // if no existing pre-release render // if exists find the bump part func (prf *preReleaseVersion) incPreRelease() (semver.Version, error) { currentPreRelease := prf.v.Prerelease() newPreRelease := currentPreRelease == "" currentPreReleaseParts := strings.Split(currentPreRelease, ".") newPreReleaseParts := []string{} // Loop over each of the format parts // Align the output to match the output for idx, fmtPart := range prf.Fmt { partArgs := strings.Split(fmtPart, " ") switch partArgs[0] { case string(BumpInt): // Handle a new pre-release if newPreRelease { newPreReleaseParts = append(newPreReleaseParts, "0") continue } // bump int // current int ++ currentVerStrPart := currentPreReleaseParts[idx] currentVerPart, err := strconv.Atoi(currentVerStrPart) // the current part is not an int, make it one if err != nil { newPreReleaseParts = append(newPreReleaseParts, "0") continue } newPreReleaseParts = append(newPreReleaseParts, strconv.Itoa(currentVerPart+1)) case string(KeyArg): // check len value, ok := prf.KeyArgs[partArgs[1]] if !ok { log.Fatalf("%s not found", partArgs[1]) return semver.Version{}, errors.New("KeyArg not found") } newPreReleaseParts = append(newPreReleaseParts, value) default: newPreReleaseParts = append(newPreReleaseParts, partArgs[0]) } } nextVersion := semver.Version{} preReleaseStr := strings.Join(newPreReleaseParts, ".") // When we are _JUST_ bumping the prerelease if !newPreRelease { newVersion, err := prf.v.SetPrerelease(preReleaseStr) if err != nil { log.Fatal(err) return semver.Version{}, err } return newVersion, nil } switch prf.versionPart { case "major": nextVersion = prf.v.IncMajor() case "minor": nextVersion = prf.v.IncMinor() case "patch": nextVersion = prf.v.IncPatch() } newVersion, err := nextVersion.SetPrerelease(preReleaseStr) if err != nil { log.Fatal(err) return semver.Version{}, err } return newVersion, nil } var ( validPartArgs = []string{"major", "minor", "patch"} preReleaseFmtArgs = make(map[string]string) preRelease bool rootCmd = &cobra.Command{ Use: "bumpver part [major|minor|patch] file [-|file]", Short: "A tool for bumping semver git tags.", Example: cmdExamples, Args: cobra.MatchAll(cobra.ExactArgs(2)), Long: `Inspired by the python bump2version tool.`, RunE: func(cmd *cobra.Command, argz []string) error { part := argz[0] var err error if err := checkArgPart(part); err != nil { return err } inputScanner, err := inOrStdinOrFile(cmd.InOrStdin(), argz[1]) if err != nil { return err } tags := []*semver.Version{} for inputScanner.Scan() { verText := inputScanner.Text() ver, err := semver.NewVersion(verText) if err != nil { continue } tags = append(tags, ver) } if len(tags) == 0 { return errors.New("No semver version found") } preRelease := viper.GetBool("prerelease") preReleaseFmtString := viper.GetString("prerelease-fmt") // remoteName := viper.GetString("remote-name") lastVersion := latestTag(tags) if (lastVersion == &semver.Version{}) { return errors.New("No tags found. Not doing anything") } versionPart := "" if preRelease { versionPart = part part = "prerelease" } // actual bump nextVersion := semver.Version{} switch part { case "major": nextVersion = lastVersion.IncMajor() case "minor": nextVersion = lastVersion.IncMinor() case "patch": nextVersion = lastVersion.IncPatch() case "prerelease": parsedFmt, err := parseFmtString(preReleaseFmtString) if err != nil { return err } preRelVersion := preReleaseVersion{ // []string{"PR", "$KeyArg PR_NUM"}, parsedFmt, preReleaseFmtArgs, lastVersion, versionPart, } nextVersion, err = preRelVersion.incPreRelease() if err != nil { return err } } fmt.Fprint(cmd.OutOrStdout(), nextVersion) return err }, } ) func latestTag(tags []*semver.Version) *semver.Version { sort.Sort(semver.Collection(tags)) numTags := len(tags) if numTags == 0 { return &semver.Version{} } return tags[numTags-1] } func inOrStdinOrFile(inputReader io.Reader, path string) (*bufio.Scanner, error) { scanner := bufio.NewScanner(inputReader) // setup file scanner if path != "-" { file, err := os.Open(path) if err != nil { return &bufio.Scanner{}, err } scanner = bufio.NewScanner(file) } scanner.Split(bufio.ScanLines) return scanner, nil } func checkArgPart(arg string) error { isValidPart := slices.Contains(validPartArgs, arg) if !isValidPart { return errors.New("A valid arg part is required") } return nil } func parseFmtString(toSplit string) ([]string, error) { splitted := strings.Split(toSplit, ".") for _, section := range splitted { keyWord := strings.Split(section, " ") if keyWord[0] == string(KeyArg) { if len(keyWord) != 2 { return []string{}, errors.New("$KeyArg expected to find an argument") } } } return strings.Split(toSplit, "."), nil } func main() { if err := rootCmd.Execute(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func init() { // global flags viper.SetConfigName(".version.toml") viper.SetConfigType("toml") viper.AddConfigPath(".") if err := viper.ReadInConfig(); err != nil { // Find and read the config file if errors.Is(err, &viper.ConfigFileNotFoundError{}) { // Handle errors reading the config file fmt.Println(err) } } cwd, err := os.Getwd() if err != nil { log.Fatal(err) } rootCmd.PersistentFlags().BoolVarP(&preRelease, "prerelease", "p", false, "create a prerelease tag for the (major|minor|patch)") rootCmd.PersistentFlags().String("prerelease-fmt", "PR.$KeyArg PR_NUM.$BumpInt", "The format string for prerelease versions") rootCmd.PersistentFlags().StringToStringVarP(&preReleaseFmtArgs, "key-args", "k", nil, "key=arg for the fmt string") rootCmd.PersistentFlags().String("repo-dir", cwd, "repo to examine") if err := viper.BindPFlags(rootCmd.PersistentFlags()); err != nil { log.Fatal(err) } }