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" ) type FmtPart string var ( BumpInt FmtPart = "$BumpInt" 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) if err != nil { return semver.Version{}, err } 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 ( errNoRemoteFound = errors.New("No remotes found") validArgs = []string{"major", "minor", "patch"} remoteName string preReleaseFmtArgs map[string]string = make(map[string]string) preRelease bool rootCmd = &cobra.Command{ Use: "semverbump part [major|minor|patch] file [-|file]", Short: "A tool for bumping semver git tags.", ValidArgs: validArgs, Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), 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 := scannerFromInput(cmd.InOrStdin()) tags := []*semver.Version{} for inputScanner.Scan() { verText := inputScanner.Text() ver, err := semver.NewVersion(verText) if err != nil { // log.debug fmt.Print(err) 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": preRelVersion := preReleaseVersion{ // []string{"PR", "$KeyArg PR_NUM"}, parseFmtString(preReleaseFmtString), 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 scannerFromInput(inputReader io.Reader) *bufio.Scanner { scanner := bufio.NewScanner(inputReader) scanner.Split(bufio.ScanLines) return scanner } func checkArgPart(arg string) error { isValidPart := slices.Contains(validArgs, arg) if !isValidPart { return errors.New("A valid arg part is required") } return nil } func parseFmtString(toSplit string) []string { return strings.Split(toSplit, ".") } 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") remoteName = *rootCmd.PersistentFlags().String("remote-name", "origin", "remote to search and push to") viper.BindPFlags(rootCmd.PersistentFlags()) }