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  }