Skip to content

Commit

Permalink
feat: expose svu functionality as a library and internal refactoring (#…
Browse files Browse the repository at this point in the history
…126)

* feat: add importable package to access functionality without cli

* docs: add docs about usage as library
  • Loading branch information
ahaasler authored Sep 19, 2023
1 parent c6f0d30 commit 337a8f2
Show file tree
Hide file tree
Showing 7 changed files with 625 additions and 397 deletions.
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,62 @@ go install github.com/caarlos0/svu@latest

Or download one from the [releases tab](https://github.com/caarlos0/svu/releases) and install manually.

## use as library

You can use `svu` as a library without the need to install the binary. For example to use it from a magefile:

```go
//go:build mage
// +build mage

package main

import (
"github.com/caarlos0/svu/pkg/svu"
"github.com/magefile/mage/sh"
"strings"
)

// Tag the current commit with the proper next semver.
func Version() error {
v, err := svu.Next()
if err != nil {
return err
}
return sh.RunV("git", "tag", "-a", v, "-m", strings.Replace(v, "v", "Version ", 1))
}
```

### commands

All commands are available with a function named accordingly:

- `svu.Next()`
- `svu.Current()`
- `svu.Major()`
- `svu.Minor()`
- `svu.Patch()`
- `svu.PreRelease()`

### options

All flags have a matching option function to configure the previous commands beyond their default bahavior:

- `svu.Current(svu.WithPattern("p*"))`
- `svu.Next(svu.WithPrefix("ver"))`
- `svu.Major(svu.StripPrefix())`
- `svu.Minor(svu.WithPreRelease("pre"))`
- `svu.Patch(svu.WithBuild("3"))`
- `svu.Next(svu.WithDirectory("internal"))`
- `svu.Next(svu.WithTagMode(svu.AllBranches))` or `svu.Next(svu.ForAllBranches())`
- `svu.Next(svu.WithTagMode(svu.CurrentBranch))` or `svu.Next(svu.ForCurrentBranch())`
- `svu.Next(svu.ForcePatchIncrement())`

Or multiple options:

- `svu.Next(svu.WithPreRelease("pre"), svu.WithBuild("3"), svu.StripPrefix())`
- `svu.PreRelease(svu.WithPreRelease("alpha.33"), svu.WithBuild("243"))`

## stargazers over time

[![Stargazers over time](https://starchart.cc/caarlos0/svu.svg)](https://starchart.cc/caarlos0/svu)
Expand Down
7 changes: 6 additions & 1 deletion internal/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import (
"github.com/gobwas/glob"
)

const (
AllBranchesTagMode = "all-branches"
CurrentBranchTagMode = "current-branch"
)

// copied from goreleaser

// IsRepo returns true if current folder is a git repository
Expand All @@ -27,7 +32,7 @@ func getAllTags(args ...string) ([]string, error) {

func DescribeTag(tagMode string, pattern string) (string, error) {
args := []string{}
if tagMode == "current-branch" {
if tagMode == CurrentBranchTagMode {
args = []string{"--merged"}
}
tags, err := getAllTags(args...)
Expand Down
170 changes: 169 additions & 1 deletion internal/svu/svu.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,186 @@
package svu

import (
"fmt"
"regexp"
"strconv"
"strings"

"github.com/caarlos0/svu/internal/git"

"github.com/Masterminds/semver"
)

const (
NextCmd = "next"
MajorCmd = "major"
MinorCmd = "minor"
PatchCmd = "patch"
CurrentCmd = "current"
PreReleaseCmd = "prerelease"
)

var (
breaking = regexp.MustCompile("(?m).*BREAKING[ -]CHANGE:.*")
breakingBang = regexp.MustCompile(`(?im).*(\w+)(\(.*\))?!:.*`)
feature = regexp.MustCompile(`(?im).*feat(\(.*\))?:.*`)
patch = regexp.MustCompile(`(?im).*fix(\(.*\))?:.*`)
)

type Options struct {
Cmd string
Pattern string
Prefix string
StripPrefix bool
PreRelease string
Build string
Directory string
TagMode string
ForcePatchIncrement bool
}

func Version(opts Options) (string, error) {
tag, err := git.DescribeTag(string(opts.TagMode), opts.Pattern)
if err != nil {
return "", fmt.Errorf("failed to get current tag for repo: %w", err)
}

current, err := getCurrentVersion(tag, opts.Prefix)
if err != nil {
return "", fmt.Errorf("could not get current version from tag: '%s': %w", tag, err)
}

result, err := nextVersion(string(opts.Cmd), current, tag, opts.PreRelease, opts.Build, opts.Directory, opts.ForcePatchIncrement)
if err != nil {
return "", fmt.Errorf("could not get next tag: '%s': %w", tag, err)
}

if opts.StripPrefix {
return result.String(), nil
}
return opts.Prefix + result.String(), nil
}

func nextVersion(cmd string, current *semver.Version, tag, preRelease, build, directory string, force bool) (semver.Version, error) {
if cmd == CurrentCmd {
return *current, nil
}

if force {
c, err := current.SetMetadata("")
if err != nil {
return c, err
}
c, err = c.SetPrerelease("")
if err != nil {
return c, err
}
current = &c
}

var result semver.Version
var err error
switch cmd {
case NextCmd, PreReleaseCmd:
result, err = findNextWithGitLog(current, tag, directory, force)
case MajorCmd:
result = current.IncMajor()
case MinorCmd:
result = current.IncMinor()
case PatchCmd:
result = current.IncPatch()
}
if err != nil {
return result, err
}

if cmd == PreReleaseCmd {
result, err = nextPreRelease(current, &result, preRelease)
if err != nil {
return result, err
}
} else {
result, err = result.SetPrerelease(preRelease)
if err != nil {
return result, err
}
}

result, err = result.SetMetadata(build)
if err != nil {
return result, err
}
return result, nil
}

func nextPreRelease(current, next *semver.Version, preRelease string) (semver.Version, error) {
suffix := ""
if preRelease != "" {
// Check if the suffix already contains a version number, if it does assume the user wants to explicitly set the version so use that
splitPreRelease := strings.Split(preRelease, ".")
if len(splitPreRelease) > 1 {
if _, err := strconv.Atoi(splitPreRelease[len(splitPreRelease)-1]); err == nil {
return current.SetPrerelease(preRelease)
}
}

suffix = preRelease

// Check if the prerelease suffix is the same as the current prerelease
preSuffix := strings.Split(current.Prerelease(), ".")[0]
if preSuffix == preRelease {
suffix = current.Prerelease()
}
} else if current.Prerelease() != "" {
suffix = current.Prerelease()
} else {
return *current, fmt.Errorf(
"--pre-release suffix is required to calculate next pre-release version as suffix could not be determined from current version: %s",
current.String(),
)
}

splitSuffix := strings.Split(suffix, ".")
preReleaseName := splitSuffix[0]
preReleaseVersion := 0

currentWithoutPreRelease, _ := current.SetPrerelease("")

if !next.GreaterThan(&currentWithoutPreRelease) {
preReleaseVersion = -1
if len(splitSuffix) == 2 {
preReleaseName = splitSuffix[0]
preReleaseVersion, _ = strconv.Atoi(splitSuffix[1])
} else if len(splitSuffix) > 2 {
preReleaseName = splitSuffix[len(splitSuffix)-1]
}

preReleaseVersion++
}

return next.SetPrerelease(fmt.Sprintf("%s.%d", preReleaseName, preReleaseVersion))
}

func getCurrentVersion(tag, prefix string) (*semver.Version, error) {
var current *semver.Version
var err error
if tag == "" {
current, err = semver.NewVersion(strings.TrimPrefix("0.0.0", prefix))
} else {
current, err = semver.NewVersion(strings.TrimPrefix(tag, prefix))
}
return current, err
}

func findNextWithGitLog(current *semver.Version, tag string, directory string, forcePatchIncrement bool) (semver.Version, error) {
log, err := git.Changelog(tag, directory)
if err != nil {
return semver.Version{}, fmt.Errorf("failed to get changelog: %w", err)
}

return findNext(current, forcePatchIncrement, log), nil
}

func isBreaking(log string) bool {
return breaking.MatchString(log) || breakingBang.MatchString(log)
}
Expand All @@ -25,7 +193,7 @@ func isPatch(log string) bool {
return patch.MatchString(log)
}

func FindNext(current *semver.Version, forcePatchIncrement bool, log string) semver.Version {
func findNext(current *semver.Version, forcePatchIncrement bool, log string) semver.Version {
if isBreaking(log) {
if current.Major() == 0 {
return current.IncMinor()
Expand Down
Loading

0 comments on commit 337a8f2

Please sign in to comment.