github.com/aserto-dev/calc-version@v1.1.4/versioning.go (about) 1 package main 2 3 import ( 4 "fmt" 5 "regexp" 6 "strconv" 7 "strings" 8 "time" 9 10 "github.com/pkg/errors" 11 ) 12 13 var ( 14 gitBinary = "git" 15 16 // Based on https://semver.org/#semantic-versioning-200 but we do support the 17 // common `v` prefix in front and do not allow plus elements like `1.0.0+gold`. 18 regexSupportedVersionFormat = regexp.MustCompile(`^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?$`) 19 20 regexMajor = regexp.MustCompile(`^([0-9]+)\.[0-9]+\.[0-9]+.*`) 21 regexMinor = regexp.MustCompile(`^[0-9]+\.([0-9]+)\.[0-9]+.*`) 22 regexPatch = regexp.MustCompile(`^[0-9]+\.[0-9]+\.([0-9]+).*`) 23 regexTail = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+(.*)`) 24 ) 25 26 func currentVersion() (string, error) { 27 err := verifyGit() 28 if err != nil { 29 return "", errors.Wrap(err, "git error") 30 } 31 32 tag, err := git("describe", "--tags", "--abbrev=0") 33 if err != nil { 34 return "", errors.Wrap(err, "exec error") 35 } 36 37 if !regexSupportedVersionFormat.MatchString(tag) { 38 if strings.Contains(tag, "+") { 39 return "", errors.Errorf("looks like your git tag '%s' has a semver with a + sign - that's not supported by this tool", tag) 40 } 41 42 return "", errors.Errorf("'%s' doesn't seem to be a semantic version", tag) 43 } 44 45 version := tag 46 47 // Version starts being the last tag that points to a commit in the branch, 48 // then it gets mutated based on a series of constraints. 49 50 // If the tag doesn't point to HEAD, it's a pre-release. 51 pointsAt, err := git("tag", "--points-at", "HEAD") 52 if err != nil { 53 return "", errors.Wrap(err, "exec error") 54 } 55 if pointsAt == "" { 56 // The commit timestamp should be in the format yyyymmddHHMMSS in UTC. 57 gitCommitTimestamp, err := git("show", "--no-patch", "--format=%ct", "HEAD") 58 if err != nil { 59 return "", errors.Wrap(err, "exec error") 60 } 61 62 unixTime, err := strconv.ParseInt(gitCommitTimestamp, 10, 64) 63 if err != nil { 64 return "", errors.Wrap(err, "failed to parse git commit timestamp") 65 } 66 parsedTimestamp := time.Unix(unixTime, 0) 67 gitCommitTimestamp = parsedTimestamp.Format("20060102150405") 68 69 // The number of commits since last tag that points to a commits in the 70 // branch. 71 gitNumberCommits, err := git("rev-list", "--count", fmt.Sprintf("%s...HEAD", version)) 72 if err != nil { 73 return "", errors.Wrap(err, "exec error") 74 } 75 76 // Add `g` to the short hash to match git describe. 77 gitCommitShortHash, err := git("rev-parse", "--short=8", "HEAD") 78 if err != nil { 79 return "", errors.Wrap(err, "exec error") 80 } 81 82 gitCommitShortHash = "g" + gitCommitShortHash 83 84 // The version gets assembled with the pre-release part. 85 version = fmt.Sprintf("%s-%s.%s.%s", version, gitCommitTimestamp, gitNumberCommits, gitCommitShortHash) 86 } 87 88 // If there's a change in the source tree that didn't get committed, append 89 // `-dirty` to the version string. 90 status, err := git("status", "--short") 91 if err != nil { 92 return "", errors.Wrap(err, "exec error") 93 } 94 if status != "" { 95 version = fmt.Sprintf("%s-dirty", version) 96 } 97 98 version = strings.TrimPrefix(version, "v") 99 100 return version, nil 101 } 102 103 func preRelease(currentVersion, identifier string) string { 104 return fmt.Sprintf("%s-%s", currentVersion, identifier) 105 } 106 107 func next(currentVersion, nextType string) (string, error) { 108 major, minor, patch, _, err := parts(currentVersion) 109 if err != nil { 110 return "", errors.Wrap(err, "failed to get version parts") 111 } 112 113 switch nextType { 114 case "patch": 115 patch = patch + 1 116 case "minor": 117 minor = minor + 1 118 patch = 0 119 case "major": 120 major = major + 1 121 minor = 0 122 patch = 0 123 default: 124 return "", errors.Errorf("Invalid value '%s' for next version. Supported values are 'patch', 'minor' and 'major'", nextType) 125 } 126 127 return fmt.Sprintf("%d.%d.%d", major, minor, patch), nil 128 } 129 130 func parts(version string) (major, minor, patch uint64, tail string, err error) { 131 matches := regexMajor.FindAllStringSubmatch(version, -1) 132 if matches == nil || len(matches) < 1 || len(matches[0]) < 2 { 133 return 0, 0, 0, "", errors.Errorf("'%s' doesn't look like a semver", version) 134 } 135 136 major, err = strconv.ParseUint(matches[0][1], 10, 64) 137 if err != nil { 138 return 0, 0, 0, "", errors.Errorf("'%s' major part of version is not a positive integer", matches[1][0]) 139 } 140 141 matches = regexMinor.FindAllStringSubmatch(version, -1) 142 if matches == nil || len(matches) < 1 || len(matches[0]) < 2 { 143 return 0, 0, 0, "", errors.Errorf("'%s' doesn't look like a semver", version) 144 } 145 146 minor, err = strconv.ParseUint(matches[0][1], 10, 64) 147 if err != nil { 148 return 0, 0, 0, "", errors.Errorf("'%s' minor part of version is not a positive integer", matches[1][0]) 149 } 150 151 matches = regexPatch.FindAllStringSubmatch(version, -1) 152 if matches == nil || len(matches) < 1 || len(matches[0]) < 2 { 153 return 0, 0, 0, "", errors.Errorf("'%s' doesn't look like a semver", version) 154 } 155 156 patch, err = strconv.ParseUint(matches[0][1], 10, 64) 157 if err != nil { 158 return 0, 0, 0, "", errors.Errorf("'%s' patch part of version is not a positive integer", matches[1][0]) 159 } 160 161 matches = regexTail.FindAllStringSubmatch(version, -1) 162 if matches == nil || len(matches) < 1 || len(matches[0]) < 2 { 163 return 0, 0, 0, "", errors.Errorf("'%s' doesn't look like a semver", version) 164 } 165 166 tail = matches[0][1] 167 168 return 169 }