package main import ( "errors" "fmt" "log" "os" "sort" "strconv" "strings" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "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") clonedRepo = &git.Repository{} localRepo = &git.Repository{} remoteName string preReleaseFmtArgs map[string]string = make(map[string]string) preRelease bool rootCmd = &cobra.Command{ Use: "semverbump part [major|minor|patch]", Short: "A tool for bumping semver git tags.", ValidArgs: []string{"major", "minor", "patch"}, 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] preRelease := viper.GetBool("prerelease") preReleaseFmtString := viper.GetString("prerelease-fmt") // remoteName := viper.GetString("remote-name") repoDir := viper.GetString("repo-dir") localRepo, err := git.PlainOpen(repoDir) if err != nil { return err } // finding tags and last version tags, err := findTags(localRepo) if err != nil { return err } 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 { numTags := len(tags) if numTags == 0 { return &semver.Version{} } return tags[numTags-1] } func findTags(repo *git.Repository) ([]*semver.Version, error) { repoTagsIter, err := repo.Tags() if err != nil { return []*semver.Version{}, err } repoTags := []*semver.Version{} if err := repoTagsIter.ForEach(func(ref *plumbing.Reference) error { rn := plumbing.ReferenceName(ref.Name()) version, err := semver.NewVersion(rn.Short()) if err != nil { // not a semver return nil } repoTags = append(repoTags, version) return nil }); err != nil { return []*semver.Version{}, err } sort.Sort(semver.Collection(repoTags)) return repoTags, 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(".") err := viper.ReadInConfig() // Find and read the config file if err != nil { // 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()) }