diff --git a/cli.go b/cli.go new file mode 100644 index 0000000..d1bbe28 --- /dev/null +++ b/cli.go @@ -0,0 +1,41 @@ +package gitsemvers + +import ( + "fmt" + "io" + "strings" + + "github.com/jessevdk/go-flags" +) + +const ( + exitcodeOK = iota + exitCodeParseFlagErr + exitCodeErr +) + +// CLI is for command line +type CLI struct { + OutStream, ErrStream io.Writer +} + +// Run the cli +func (cli *CLI) Run(argv []string) int { + p, sv, err := parseArgs(argv) + if err != nil { + if ferr, ok := err.(*flags.Error); !ok || ferr.Type != flags.ErrHelp { + p.WriteHelp(cli.ErrStream) + } + return exitCodeParseFlagErr + } + fmt.Fprintln(cli.OutStream, strings.Join(sv.VersionStrings(), "\n")) + return exitcodeOK +} + +func parseArgs(args []string) (*flags.Parser, *Semvers, error) { + sv := &Semvers{} + p := flags.NewParser(sv, flags.Default) + p.Usage = "[OPTIONS]\n\nVersion: " + version + _, err := p.ParseArgs(args) + return p, sv, err +} diff --git a/cmd/git-semvers/git-semvers.go b/cmd/git-semvers/git-semvers.go new file mode 100644 index 0000000..cd5efd8 --- /dev/null +++ b/cmd/git-semvers/git-semvers.go @@ -0,0 +1,11 @@ +package main + +import ( + "os" + + "github.com/Songmu/gitsemvers" +) + +func main() { + os.Exit((&gitsemvers.CLI{ErrStream: os.Stderr, OutStream: os.Stdout}).Run(os.Args[1:])) +} diff --git a/gitsemvers.go b/gitsemvers.go new file mode 100644 index 0000000..3762dc0 --- /dev/null +++ b/gitsemvers.go @@ -0,0 +1,117 @@ +package gitsemvers + +import ( + "bytes" + "os" + "os/exec" + "regexp" + "sort" + "strings" + + "github.com/Masterminds/semver" +) + +const version = "0.0.0" + +var verRegStr = `^v?[0-9]+(?:\.[0-9]+){0,2}` +var extension = `[-0-9A-Za-z]+(?:\.[-0-9A-Za-z]+)*` +var withPreReleaseRegStr = "(?:-" + extension + ")?" +var withBuildMetadataRegStr = `(?:\+` + extension + ")?" + +type regBuilder uint + +const ( + naked regBuilder = 0 + + withPreRelease = 1 << (iota - 1) + withBuildMetadata + + withPreReleaseAndBuildMetadata = withPreRelease | withBuildMetadata +) + +var cache = make(map[regBuilder]*regexp.Regexp) + +func (rb regBuilder) build() string { + b := bytes.NewBufferString(verRegStr) + if rb&withPreRelease != 0 { + b.WriteString(withPreReleaseRegStr) + } + if rb&withBuildMetadata != 0 { + b.WriteString(withBuildMetadataRegStr) + } + b.WriteString("$") + return b.String() +} + +func (rb regBuilder) reg() *regexp.Regexp { + return cache[rb] +} + +func init() { + regs := []regBuilder{naked, withPreRelease, withBuildMetadata, withPreReleaseAndBuildMetadata} + for _, v := range regs { + cache[v] = regexp.MustCompile(v.build()) + } +} + +// Semvers retrieve semvers from git tags +type Semvers struct { + RepoPath string `short:"r" long:"repo" default:"." description:"git repository path"` + GitPath string `short:"g" long:"git" default:"git" description:"git path"` + WithPreRelease bool `short:"P" long:"with-pre-release" description:"display pre-release versions"` + WithBuildMetadata bool `short:"B" long:"with-build-metadata" description:"display build-metadata versions"` +} + +// VersionStrings returns version strings +func (sv *Semvers) VersionStrings() []string { + tags, err := sv.gitTags() + if err != nil { + return nil + } + return sv.parseVersions(tags) +} + +func (sv *Semvers) reg() *regexp.Regexp { + regB := regBuilder(0) + if sv.WithPreRelease { + regB |= withPreRelease + } + if sv.WithBuildMetadata { + regB |= withBuildMetadata + } + return regB.reg() +} + +func (sv *Semvers) gitProg() string { + if sv.GitPath != "" { + return sv.GitPath + } + return "git" +} + +func (sv *Semvers) gitTags() (string, error) { + cmd := exec.Command(sv.gitProg(), "-C", sv.RepoPath, "tag") + var b bytes.Buffer + cmd.Stdout = &b + cmd.Stderr = os.Stderr + err := cmd.Run() + return b.String(), err +} + +func (sv *Semvers) parseVersions(out string) []string { + rawTags := strings.Split(out, "\n") + var versions []*semver.Version + for _, tag := range rawTags { + t := strings.TrimSpace(tag) + if sv.reg().MatchString(t) { + v, _ := semver.NewVersion(t) + versions = append(versions, v) + } + } + sort.Sort(sort.Reverse(semver.Collection(versions))) + var vers = make([]string, len(versions)) + for i, v := range versions { + vers[i] = v.Original() + } + return vers +} diff --git a/gitsemvers_test.go b/gitsemvers_test.go new file mode 100644 index 0000000..5dcead3 --- /dev/null +++ b/gitsemvers_test.go @@ -0,0 +1,74 @@ +package gitsemvers + +import ( + "reflect" + "testing" +) + +var input = `dummy +v0.10.1 +v0.9.0 +v0.9.3 +v0.8.4-pre +v0.8.4 +v0.8.3+win +v0.8.2-pre.pre+win.win +v0.7.0-pre+win+invalid +` + +func TestParseVersions(t *testing.T) { + expect := []string{ + "v0.10.1", + "v0.9.3", + "v0.9.0", + "v0.8.4", + } + sv := &Semvers{} + if !reflect.DeepEqual(sv.parseVersions(input), expect) { + t.Errorf("something went wrong %+v", sv.parseVersions(input)) + } +} + +func TestParseVersionsWithPreRelease(t *testing.T) { + expect := []string{ + "v0.10.1", + "v0.9.3", + "v0.9.0", + "v0.8.4", + "v0.8.4-pre", + } + sv := &Semvers{WithPreRelease: true} + if !reflect.DeepEqual(sv.parseVersions(input), expect) { + t.Errorf("something went wrong %+v", sv.parseVersions(input)) + } +} + +func TestParseVersionsWithBuildMetadata(t *testing.T) { + expect := []string{ + "v0.10.1", + "v0.9.3", + "v0.9.0", + "v0.8.4", + "v0.8.3+win", + } + sv := &Semvers{WithBuildMetadata: true} + if !reflect.DeepEqual(sv.parseVersions(input), expect) { + t.Errorf("something went wrong %+v", sv.parseVersions(input)) + } +} + +func TestParseVersionsWithAllExtensions(t *testing.T) { + expect := []string{ + "v0.10.1", + "v0.9.3", + "v0.9.0", + "v0.8.4", + "v0.8.4-pre", + "v0.8.3+win", + "v0.8.2-pre.pre+win.win", + } + sv := &Semvers{WithPreRelease: true, WithBuildMetadata: true} + if !reflect.DeepEqual(sv.parseVersions(input), expect) { + t.Errorf("something went wrong %+v", sv.parseVersions(input)) + } +}