diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml deleted file mode 100644 index daeeb50..0000000 --- a/.github/workflows/cd.yaml +++ /dev/null @@ -1,106 +0,0 @@ -name: CD -on: - push: - branches: - - master - -jobs: - prebuild: - name: Prebuild - runs-on: ubuntu-latest - outputs: - newversion: ${{ env.NEW_VERSION }} - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - ref: ${{ github.ref }} - fetch-depth: 0 # Include all history as we need that to determine the next version - - name: Determine New Version - run: echo "NEW_VERSION=$(make local-version)" >> $GITHUB_ENV - - build: - name: Build - runs-on: ubuntu-latest - needs: - prebuild - strategy: - matrix: - os: [darwin, linux] - arch: [amd64] - outputs: - newversion: ${{ needs.prebuild.outputs.newversion }} - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - ref: ${{ github.ref }} - - name: Bootstrap - run: make bootstrap - - name: Build - run: make NEW_VERSION=${{ needs.prebuild.outputs.newversion }} - env: - TARGET_OS: ${{ matrix.os }} - TARGET_ARCH: ${{ matrix.arch }} - - name: Save Artifacts - uses: actions/upload-artifact@v2 - with: - name: ccp-${{ matrix.os }}-${{ matrix.arch }} - path: bin/ccp - - release: - name: Release - runs-on: ubuntu-latest - needs: - build - outputs: - uploadurl: ${{ steps.create-release.outputs.upload_url }} - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - ref: ${{ github.ref }} - fetch-depth: 0 - - name: Tag - run: | - git config user.name github-actions - git config user.email github-actions@github.com - git tag -a ${{ needs.build.outputs.newversion }} -m ${{ needs.build.outputs.newversion }} - git push --tags - - name: Create Release - id: create-release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ needs.build.outputs.newversion }} - release_name: v${{ needs.build.outputs.newversion }} - draft: false - prerelease: false - - upload-artifacts: - name: Upload Artifacts - runs-on: ubuntu-latest - needs: - release - strategy: - matrix: - os: [darwin, linux] - arch: [amd64] - steps: - - name: Download Artifact - uses: actions/download-artifact@v2 - with: - name: ccp-${{ matrix.os }}-${{ matrix.arch }} - - name: Make Artifact Executable # See https://github.com/actions/upload-artifact/issues/38 - run: chmod +x ./ccp - - name: Upload Asset - id: upload-asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.release.outputs.uploadurl }} - asset_path: ./ccp - asset_name: ccp-${{ matrix.os }}-${{ matrix.arch }} - asset_content_type: "application" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index 2ac7bd4..0000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,22 +0,0 @@ -name: CI -on: - pull_request: - branches: - - master - -jobs: - validate: - name: Validate - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - ref: ${{ github.ref }} - - name: Build - run: make alpha # No need to bootstrap - - name: Upload - uses: actions/upload-artifact@v2 - with: - name: ccp - path: bin/ccp diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..5d7e27b --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,34 @@ +name: Go + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + types: + - opened + - synchronize + - reopened + - review_requested + pull_request_review: + types: + - submitted + workflow_dispatch: + +jobs: + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.20' + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -v ./... diff --git a/LICENSE.md b/LICENSE.md index 33584ae..1aba092 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,7 @@ -MIT License +The MIT License (MIT) -Copyright (c) 2021 Matthew Bamber +Copyright (c) 2021 Muthu Krishnan +Copyright (c) 2021 Matthew Bamber, Rene Zbinden, Brandon Buck Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile deleted file mode 100644 index 5797273..0000000 --- a/Makefile +++ /dev/null @@ -1,29 +0,0 @@ -GOBIN ?= ${PWD} -NEW_VERSION ?= 0.1.0 -TARGET_OS ?= $(shell go env GOOS) -TARGET_ARCH ?= $(shell go env GOARCH) - -.DEFAULT_GOAL := build - -.PHONY: bootstrap -bootstrap: - @GOBIN=${GOBIN} go get github.com/mitchellh/gox - @go mod download - -.PHONY: clean -clean: - @rm -rf bin - -.PHONY: alpha -alpha: clean - @mkdir -p bin - @go build -o bin -ldflags="-X main.applicationVersion=$(shell date +%s)-alpha" - -.PHONY: build -build: clean - @mkdir -p bin - @${GOBIN}/gox -os=${TARGET_OS} -arch=${TARGET_ARCH} -output bin/ccp -ldflags="-X main.applicationVersion=${NEW_VERSION}" - -.PHONY: local-version -local-version: - @go run . version diff --git a/README.md b/README.md index 3706a7d..61ff3d7 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,60 @@ -# Cov Commit Parser -![CD](https://github.com/mbamber/cov-commit-parser/workflows/CD/badge.svg) +# Parser -A simple parser for [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). +A go parser for [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) messages -## Usage -When run without any arguments, `ccp version` will parse the commits at the current HEAD and output a single line containing a version number. This version number is the recommended version to use for the next build, based on the commit messages included since the latest tag on the branch. +[![PkgGoDev](https://pkg.go.dev/badge/github.com/conventionalcommit/parser)](https://pkg.go.dev/github.com/conventionalcommit/parser)![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/conventionalcommit/parser) -The current version number is determined by the tag representing the latest semantic version. This can be overridden using the `--current` flag. +### Usage -Use the `--since` flag to specify a commit hash, branch name or tag to adjust the commits used during the parsing. - -### Use as a library -The tool can also be embedded into existing Go programs. The example below returns a new version based on the starting version `1.0.0` and using the single commit on the `HEAD` of a git repository in the directory `repo_path`: ```go -commitMessages, err := git.GetCommitsInDirectory("repo_path", "HEAD~1", "HEAD") -if err != nil { - fmt.Printf("Error: %s", err.Error()) -} +var msg = `feat(scope): description -v, err := ccp.GetNextVersion("1.0.0", commitMessages, ccp.DefaultPatchTypes) +this is first line in body + +this is second line in body + +Ref #123 +Date: 01-01-2021 +By: John Doe` + +commit, err := Parse(msg) if err != nil { fmt.Printf("Error: %s", err.Error()) } +fmt.Printf("%#v", commit) -fmt.Println(v) +/* +commitMsg = &parser.Commit{ + message: "feat(scope): description\n\nthis is first line in body\n\nthis is second line in body\n\nRef #123\nDate: 01-01-2021\nBy: John Doe", + header: "feat(scope): description", + body: "this is first line in body\n\nthis is second line in body", + footer: "Ref #123\nDate: 01-01-2021\nBy: John Doe", + commitType: "feat", + scope: "scope", + description: "description", + notes: { + {token:"Ref", value:"123"}, + {token:"Date", value:"01-01-2021"}, + {token:"By", value:"John Doe"}, + }, + isBreakingChange: false, +} +*/ ``` + +### TODO + +- [ ] More Test Cases +- [ ] Benchmark + +### Attribution + +This parser is inspired and forked from + +- [cc](https://github.com/zbindenren/cc) by [Rene Zbinden](https://github.com/zbindenren) +- [go-lexer](https://github.com/bbuck/go-lexer) by [Brandon Buck](https://github.com/bbuck) +- [cov-commit-parser](https://github.com/mbamber/cov-commit-parser) by [Matthew Bamber](https://github.com/mbamber) + +### License + +[MIT License](https://github.com/conventionalcommit/parser/tree/master/LICENSE.md) diff --git a/cmd.go b/cmd.go deleted file mode 100644 index 801871a..0000000 --- a/cmd.go +++ /dev/null @@ -1,98 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/mbamber/ccp/git" - "github.com/spf13/cobra" - "github.com/xfxdev/xlog" -) - -// flags -var ( - current string - directory string - since string - verbose bool -) - -var rootCmd = &cobra.Command{ - Use: "ccp", - Short: "Cov Commit Parser is a simple tool for parsing conventional commits", - Long: "A simple tool for parsing conventional commits. The full specification for conventional commits is available at https://conventionalcommits.org/en/v1.0.0", - Version: func() string { return applicationVersion }(), // Use a func() here so we can override the variable using linker - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - xlog.SetLevel(xlog.InfoLevel) - if verbose { - xlog.SetLevel(xlog.DebugLevel) - } - return nil - }, -} - -var versionCmd = &cobra.Command{ - Use: "version", - Short: "Determine the next build number to use", - Long: "Determine the next build number to use based on git tags or specified verisons", - RunE: runVersionCmd, -} - -func runVersionCmd(cmd *cobra.Command, args []string) (err error) { - // Get the directory of the git repo. Default to the current working directory - if directory == "" { - directory, err = os.Getwd() - if err != nil { - return err - } - } - xlog.Debugf("Using directory %s", directory) - - // Get the latest version tag in the git repo - latestVersion, err := git.GetLatestVersionInDirectory(directory) - if err != nil { - if current == "" || since == "" { // We only need this if either `current` or `since` were not provided - xlog.Warn("Unable to determine current version so suggesting initial version of 0.1.0") - fmt.Println("0.1.0") - return nil - } - } - - // Get the current version number. Default to the latest tag on the branch - if current == "" { - current = latestVersion - } - xlog.Debugf("Using current version %s", current) - - // Get the start point - if since == "" { - since = latestVersion - } - xlog.Debugf("Discovering commits since %s", since) - - // Discover the commits - commits, err := git.GetCommitsInDirectory(since, "HEAD", directory) - if err != nil { - return err - } - - // Compute the next version - v, err := GetNextVersion(current, commits, DefaultPatchTypes) - if err != nil { - return err - } - - // Print the version - fmt.Println(v) - - return nil -} - -func setup() { - rootCmd.PersistentFlags().StringVarP(¤t, "current", "c", "", "Current version number from which to base the version change. Defaults to the latest version tag in the repository") - rootCmd.PersistentFlags().StringVarP(&directory, "directory", "d", "", "Directory of the git repository") - rootCmd.PersistentFlags().StringVarP(&since, "since", "s", "", "Revision to track commits from. Defaults to the latest version tag in the repository") - rootCmd.PersistentFlags().BoolVar(&verbose, "verbose", false, "Enable verbose logging") - - rootCmd.AddCommand(versionCmd) -} diff --git a/commit.go b/commit.go new file mode 100644 index 0000000..986b4e1 --- /dev/null +++ b/commit.go @@ -0,0 +1,85 @@ +package parser + +// Commit represents a commit that adheres to the conventional commits specification +type Commit struct { + message string + + header string + body string + footer string + + commitType string + scope string + description string + notes []Note + + isBreakingChange bool +} + +// Message returns input commit message +func (c *Commit) Message() string { + return c.message +} + +// Header returns header of the commit +func (c *Commit) Header() string { + return c.header +} + +// Body returns body of the commit +func (c *Commit) Body() string { + return c.body +} + +// Footer returns footer of the commit +func (c *Commit) Footer() string { + return c.footer +} + +// Type returns type of the commit +func (c *Commit) Type() string { + return c.commitType +} + +// Scope returns scope of the commit +func (c *Commit) Scope() string { + return c.scope +} + +// Description returns description of the commit +func (c *Commit) Description() string { + return c.description +} + +// Notes returns footer notes of the commit +func (c *Commit) Notes() []Note { + return c.notes +} + +// IsBreakingChange returns true if commit is breaking change +func (c *Commit) IsBreakingChange() bool { + return c.isBreakingChange +} + +// Note represents one footer note +type Note struct { + token string + value string +} + +func newNote(token, value string) Note { + return Note{ + token: token, + value: value, + } +} + +// Token returns the token of the Footer Note +func (n *Note) Token() string { + return n.token +} + +// Value returns the value of the Footer Note +func (n *Note) Value() string { + return n.value +} diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..c5e19a3 --- /dev/null +++ b/example_test.go @@ -0,0 +1,28 @@ +package parser_test + +import ( + "fmt" + + "github.com/conventionalcommit/parser" +) + +func ExampleParser() { + var msg = `feat(scope): description + +this is first line in body + +this is second line in body + +Ref #123 +Date: 01-01-2021 +By: John Doe` + + p := parser.New() + commit, err := p.Parse(msg) + if err != nil { + fmt.Printf("Error: %s", err.Error()) + } + fmt.Printf("%#v", commit) + + // Output: &parser.Commit{message:"feat(scope): description\n\nthis is first line in body\n\nthis is second line in body\n\nRef #123\nDate: 01-01-2021\nBy: John Doe", header:"feat(scope): description", body:"this is first line in body\n\nthis is second line in body", footer:"Ref #123\nDate: 01-01-2021\nBy: John Doe", commitType:"feat", scope:"scope", description:"description", notes:[]parser.Note{parser.Note{token:"Ref", value:"123"}, parser.Note{token:"Date", value:"01-01-2021"}, parser.Note{token:"By", value:"John Doe"}}, isBreakingChange:false} +} diff --git a/git/git.go b/git/git.go deleted file mode 100644 index 47a10c5..0000000 --- a/git/git.go +++ /dev/null @@ -1,111 +0,0 @@ -package git - -import ( - "errors" - - "github.com/Masterminds/semver" - "github.com/xfxdev/xlog" - "gopkg.in/src-d/go-git.v4" - "gopkg.in/src-d/go-git.v4/plumbing" -) - -// GetCommitsInDirectory returns all the commit messages between the two refs -// (exluding `from`, but including `to`). The order of the returned commits is -// not guaranteed -func GetCommitsInDirectory(from, to, directory string) ([]string, error) { - // Load the repository - r, err := git.PlainOpen(directory) - if err != nil { - return nil, err - } - - return getCommits(r, from, to) -} - -// getCommits returns all the commit messages for the given repository between the two -// refs (exluding `from`, but including `to`) -func getCommits(r *git.Repository, from, to string) ([]string, error) { - // Resolve the revisions to hashes - xlog.Debugf("Resolving revision %s", to) - toHash, err := r.ResolveRevision(plumbing.Revision(to)) - if err != nil { - return nil, err - } - - xlog.Debugf("Resolving revision %s", from) - fromHash, err := r.ResolveRevision(plumbing.Revision(from)) - if err != nil { - return nil, err - } - - // Get an iterator for the commit messages - iter, err := r.Log(&git.LogOptions{ - From: *toHash, - }) - if err != nil { - return nil, err - } - - // Record all the commit messages - messages := []string{} - for { - c, err := iter.Next() - if err != nil { - xlog.Debug("No more messages to parse") - break - } - - if c.Hash.String() == fromHash.String() { - break - } - - messages = append(messages, c.Message) - } - xlog.Debugf("Got messages: %v", messages) - return messages, nil -} - -// GetLatestVersionInDirectory retrieves the latest version tag for the repository in the given directory -func GetLatestVersionInDirectory(directory string) (string, error) { - // Load the repository - r, err := git.PlainOpen(directory) - if err != nil { - return "", err - } - - return getLatestVersion(r) -} - -// getLatestVersion retrieves the latest version tag for the given repository -func getLatestVersion(r *git.Repository) (tag string, err error) { - iter, err := r.Tags() - if err != nil { - return "", err - } - - latestTag := semver.MustParse("0.0.0") - err = iter.ForEach(func(ref *plumbing.Reference) error { - v, err := semver.NewVersion(ref.Name().Short()) - if err != nil { - xlog.Debugf("Ignoring tag %s as it is not a valid semver", ref.Name().Short()) - return nil - } - - if v.GreaterThan(latestTag) { - latestTag = v - return nil - } - - // version was older than current version - return nil - }) - if err != nil { - return "", err - } - - if latestTag.String() == "0.0.0" { - return "", errors.New("Unable to find latest version") - } - - return latestTag.String(), nil -} diff --git a/git/git_internal_test.go b/git/git_internal_test.go deleted file mode 100644 index 4bdbe74..0000000 --- a/git/git_internal_test.go +++ /dev/null @@ -1,391 +0,0 @@ -package git - -import ( - "testing" - - "github.com/jmcvetta/randutil" - "github.com/stretchr/testify/assert" - "github.com/xfxdev/xlog" - "gopkg.in/src-d/go-billy.v4/memfs" - "gopkg.in/src-d/go-git.v4" - "gopkg.in/src-d/go-git.v4/config" - "gopkg.in/src-d/go-git.v4/plumbing" - "gopkg.in/src-d/go-git.v4/plumbing/object" - "gopkg.in/src-d/go-git.v4/storage/memory" -) - -var ( - signature = object.Signature{ - Name: "Golang Tests", - Email: "gotest@noreply.com", - } -) - -// createRepository creates an in-memory repository that can be used for testing -func createRepository(t *testing.T) *git.Repository { - // Create the repo - r, err := git.Init(memory.NewStorage(), memfs.New()) - if err != nil { - t.Errorf("Unable to create repository for testing: %s", err.Error()) - } - - return r -} - -// createBranch creates and checks out a new branch -func createBranch(t *testing.T, r *git.Repository, branch string) { - err := r.CreateBranch(&config.Branch{ - Name: branch, - }) - if err != nil { - t.Errorf("Unable to create branch %s: %s", branch, err.Error()) - } - xlog.Debugf("Created branch %s", branch) - - checkout(t, r, branch) -} - -// checkout checksout the given branch name -func checkout(t *testing.T, r *git.Repository, branch string) { - w, err := r.Worktree() - if err != nil { - t.Errorf("Unable to retrieve worktree: %s", err.Error()) - } - - w.Checkout(&git.CheckoutOptions{ - Branch: plumbing.NewBranchReferenceName(branch), - }) -} - -// createCommit creates a commit on the current branch -func createCommit(t *testing.T, r *git.Repository, message string) { - w, err := r.Worktree() - if err != nil { - t.Errorf("Unable to retrieve worktree: %s", err.Error()) - } - - // Create a file with a random name - s, err := randutil.AlphaString(32) - if err != nil { - t.Errorf("Unable to generate random string: %s", err.Error()) - } - f, err := w.Filesystem.Create(s) - if err != nil { - t.Errorf("Unable to create file in worktree: %s", err.Error()) - } - xlog.Debugf("Created file %s", f.Name()) - - // Stage the file - _, err = w.Add(f.Name()) - if err != nil { - t.Errorf("Unable to stage file %s: %s", f.Name(), err.Error()) - } - - _, err = w.Commit(message, &git.CommitOptions{ - Author: &signature, - Committer: &signature, - }) - if err != nil { - t.Errorf("Unable to commit file %s: %s", f.Name(), err.Error()) - } -} - -func createTag(t *testing.T, r *git.Repository, tag string) { - head, err := r.Head() - if err != nil { - t.Error("Unable to get HEAD") - } - - _, err = r.CreateTag(tag, head.Hash(), &git.CreateTagOptions{ - Tagger: &signature, - Message: tag, - }) - if err != nil { - t.Errorf("Unable to create tag %s: %s", tag, err.Error()) - } - xlog.Debugf("Created tag %s", tag) -} - -func TestGetCommits(t *testing.T) { - const ( - b1 = "b1" - b2 = "b2" - b3 = "b3" - - c1 = "first commit" - c2 = "second commit" - c3 = "third commit" - - v1 = "1.0.0" - - head = "HEAD" - head1 = "HEAD~1" - head2 = "HEAD~2" - ) - - cases := map[string]struct { - setup func(*testing.T) *git.Repository - from string - to string - expected []string - }{ - "simple": { - setup: func(t *testing.T) *git.Repository { - r := createRepository(t) - createBranch(t, r, b1) - createCommit(t, r, c1) - createCommit(t, r, c2) - createCommit(t, r, c3) - return r - }, - from: head1, - to: head, - expected: []string{ - c3, - }, - }, - "multiple commits": { - setup: func(t *testing.T) *git.Repository { - r := createRepository(t) - createBranch(t, r, b1) - createCommit(t, r, c1) - createCommit(t, r, c2) - createCommit(t, r, c3) - return r - }, - from: head2, - to: head, - expected: []string{ - c2, - c3, - }, - }, - "truncated branch": { - setup: func(t *testing.T) *git.Repository { - r := createRepository(t) - createBranch(t, r, b1) - createCommit(t, r, c1) - createCommit(t, r, c2) - createCommit(t, r, c3) - return r - }, - from: head2, - to: head1, - expected: []string{ - c2, - }, - }, - "from and to are equal": { - setup: func(t *testing.T) *git.Repository { - r := createRepository(t) - createBranch(t, r, b1) - createCommit(t, r, c1) - return r - }, - from: head, - to: head, - expected: []string{}, - }, - "from tag": { - setup: func(t *testing.T) *git.Repository { - r := createRepository(t) - createBranch(t, r, b1) - createCommit(t, r, c1) - createTag(t, r, v1) - createCommit(t, r, c2) - createCommit(t, r, c3) - return r - }, - from: v1, - to: head, - expected: []string{ - c2, - c3, - }, - }, - "to before from": { - setup: func(t *testing.T) *git.Repository { - r := createRepository(t) - createBranch(t, r, b1) - createCommit(t, r, c1) - createCommit(t, r, c2) - createCommit(t, r, c3) - return r - }, - from: head, - to: head2, - expected: []string{ - c1, - }, - }, - } - - for name, data := range cases { - r := data.setup(t) - commits, err := getCommits(r, data.from, data.to) - if assert.NoErrorf(t, err, name) { - assert.Lenf(t, commits, len(data.expected), name) - - // Check all the elements of data.expected are present, but don't worry about the order - for _, c := range data.expected { - assert.Containsf(t, commits, c, name) - } - } - } -} - -func TestGetCommitsError(t *testing.T) { - const ( - b1 = "b1" - - c1 = "first commit" - c2 = "second commit" - c3 = "third commit" - - head = "HEAD" - head1 = "HEAD~1" - head2 = "HEAD~2" - ) - - cases := map[string]struct { - setup func(*testing.T) *git.Repository - from string - to string - }{ - "bad from": { - setup: func(t *testing.T) *git.Repository { - r := createRepository(t) - createBranch(t, r, b1) - createCommit(t, r, c1) - return r - }, - from: "bad", - to: head, - }, - "bad to": { - setup: func(t *testing.T) *git.Repository { - r := createRepository(t) - createBranch(t, r, b1) - createCommit(t, r, c1) - return r - }, - from: head1, - to: "bad", - }, - } - - for name, data := range cases { - r := data.setup(t) - _, err := getCommits(r, data.from, data.to) - assert.Errorf(t, err, name) - } -} - -func TestGetLatestVersion(t *testing.T) { - const ( - b1 = "b1" - b2 = "b2" - - c1 = "first commit" - c2 = "second commit" - - v0 = "0.1.0" - v1 = "1.0.0" - v2 = "2.0.0" - ) - - cases := map[string]struct { - setup func(*testing.T) *git.Repository - expected string - }{ - "simple": { - setup: func(t *testing.T) *git.Repository { - r := createRepository(t) - createBranch(t, r, b1) - createCommit(t, r, c1) - createTag(t, r, v1) - return r - }, - expected: v1, - }, - "multiple tags": { - setup: func(t *testing.T) *git.Repository { - r := createRepository(t) - createBranch(t, r, b1) - createCommit(t, r, c1) - createTag(t, r, v1) - createTag(t, r, v2) - return r - }, - expected: v2, - }, - "multiple tags on multiple branches": { - setup: func(t *testing.T) *git.Repository { - r := createRepository(t) - createBranch(t, r, b1) - createCommit(t, r, c1) - createTag(t, r, v1) - createBranch(t, r, b2) - createCommit(t, r, c2) - createTag(t, r, v2) - return r - }, - expected: v2, - }, - "contains invalid semver": { - setup: func(t *testing.T) *git.Repository { - r := createRepository(t) - createBranch(t, r, b1) - createCommit(t, r, c1) - createTag(t, r, v1) - createTag(t, r, "Invalid Semver") - return r - }, - expected: v1, - }, - } - - for name, data := range cases { - r := data.setup(t) - v, err := getLatestVersion(r) - if assert.NoError(t, err) { - assert.Equalf(t, data.expected, v, name) - } - } -} - -func TestGetLatestVersionError(t *testing.T) { - const ( - b1 = "b1" - - c1 = "first commit" - ) - - cases := map[string]struct { - setup func(*testing.T) *git.Repository - }{ - "no valid semvers": { - setup: func(*testing.T) *git.Repository { - r := createRepository(t) - createBranch(t, r, b1) - createCommit(t, r, c1) - createTag(t, r, "Invalid Semver") - return r - }, - }, - "no tags": { - setup: func(*testing.T) *git.Repository { - r := createRepository(t) - createBranch(t, r, b1) - createCommit(t, r, c1) - return r - }, - }, - } - - for name, data := range cases { - r := data.setup(t) - _, err := getLatestVersion(r) - assert.Errorf(t, err, name) - } -} diff --git a/go.mod b/go.mod index 952d6bb..35c5f02 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,3 @@ -module github.com/mbamber/ccp +module github.com/conventionalcommit/parser -go 1.13 - -require ( - github.com/Masterminds/semver v1.5.0 - github.com/hashicorp/go-version v1.2.1 // indirect - github.com/jmcvetta/randutil v0.0.0-20150817122601-2bb1b664bcff - github.com/mitchellh/gox v1.0.1 // indirect - github.com/spf13/cobra v1.0.0 - github.com/stretchr/testify v1.6.1 - github.com/xfxdev/xlog v0.0.0-20190115101715-8752a0193860 - golang.org/x/mod v0.3.0 // indirect - gopkg.in/src-d/go-billy.v4 v4.3.2 - gopkg.in/src-d/go-git.v4 v4.13.1 -) +go 1.17 diff --git a/go.sum b/go.sum index 2870c99..e69de29 100644 --- a/go.sum +++ b/go.sum @@ -1,222 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= -github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= -github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= -github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= -github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/go-version v1.0.0 h1:21MVWPKDphxa7ineQQTrCU5brh7OuVVAzGOCnnCPtE8= -github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= -github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jmcvetta/randutil v0.0.0-20150817122601-2bb1b664bcff h1:6NvhExg4omUC9NfA+l4Oq3ibNNeJUdiAF3iBVB0PlDk= -github.com/jmcvetta/randutil v0.0.0-20150817122601-2bb1b664bcff/go.mod h1:ddfPX8Z28YMjiqoaJhNBzWHapTHXejnB5cDCUWDwriw= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= -github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/gox v1.0.1 h1:x0jD3dcHk9a9xPSDN6YEL4xL6Qz0dvNYm8yZqui5chI= -github.com/mitchellh/gox v1.0.1/go.mod h1:ED6BioOGXMswlXa2zxfh/xdd5QhwYliBFn9V18Ap4z4= -github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= -github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= -github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= -github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= -github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= -github.com/xfxdev/xlog v0.0.0-20190115101715-8752a0193860 h1:IRDKGKJ0k0SaKKKPM9y3ykWboDFGfHsghdDnmDfKyms= -github.com/xfxdev/xlog v0.0.0-20190115101715-8752a0193860/go.mod h1:IlUWb+dbGFMBVgiAIHe0zlPhYvV9Wju4W9OLFJEsfnQ= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e h1:D5TXcfTk7xF7hvieo4QErS3qqCB4teTffacDWr7CI+0= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= -gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= -gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg= -gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= -gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= -gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= -gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/helpers_test.go b/helpers_test.go deleted file mode 100644 index b133e3e..0000000 --- a/helpers_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package main_test - -import ( - "fmt" - "io/ioutil" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -// loadStringFromFile loads a file and returns the entire contents as a string. Any -// leading or trailing whitespace is removed -func loadStringFromFile(t *testing.T, dir string) string { - t.Helper() - - nameParts := strings.Split(t.Name(), "/") - filename := fmt.Sprintf("%s/%s", dir, nameParts[len(nameParts)-1]) - out, err := ioutil.ReadFile(filename) - if err != nil { - assert.Failf(t, "error in test setup", "unable to load file %s", filename) - } - - return strings.TrimSpace(string(out)) -} diff --git a/lexer.go b/lexer.go new file mode 100644 index 0000000..e6a36c6 --- /dev/null +++ b/lexer.go @@ -0,0 +1,190 @@ +package parser + +import ( + "strings" + "unicode/utf8" +) + +const ( + eof rune = -1 + tokenChBufSize = 10 + runeStackBufSize = 64 +) + +type stateFunc func(*lexer) stateFunc + +type tokenType int + +type token struct { + Type tokenType + Value string + Start, End int +} + +type lexer struct { + source string + startPos, currentPos int + runeStack []rune + + startState stateFunc + tokenCh chan token + + err error + errorHandler func(err error) +} + +// newLexer creates a returns a lexer ready to parse the given source code. +func newLexer(src string, start stateFunc, errHand func(err error)) *lexer { + return &lexer{ + source: src, + startState: start, + startPos: 0, + currentPos: 0, + errorHandler: errHand, + runeStack: make([]rune, runeStackBufSize), + } +} + +func (l *lexer) Start() { + l.tokenCh = make(chan token, tokenChBufSize) + + go l.start() +} + +func (l *lexer) start() { + state := l.startState + for state != nil { + state = state(l) + } + close(l.tokenCh) +} + +// NextToken returns the next token from the lexer and a value to denote whether +// or not the token is finished. +func (l *lexer) NextToken() (*token, bool) { + tok, ok := <-l.tokenCh + if ok { + return &tok, false + } + return nil, true +} + +// Error if an errorHandler is given, sets lex.Err with given error and calls errorHandler +// if no errorHandler is given, then it panics with given error. +func (l *lexer) Error(e error) { + if l.errorHandler == nil { + panic(e) + } + + l.err = e + l.errorHandler(e) +} + +// Current returns the value being being analyzed at this moment. +func (l *lexer) Current() string { + return l.source[l.startPos:l.currentPos] +} + +// Current returns the value being being analyzed at this moment. +func (l *lexer) Get(startPos, endPos int) string { + return l.source[startPos:endPos] +} + +func (l *lexer) Err() error { + return l.err +} + +// Emit will receive a token type and push a new token with the current analyzed +// value into the tokens channel. +func (l *lexer) Emit(t tokenType) { + tok := token{ + Type: t, + Value: l.Current(), + Start: l.startPos, + End: l.currentPos, + } + l.tokenCh <- tok + l.startPos = l.currentPos + l.clearRune() +} + +// Ignore clears the rewind stack and then sets the current beginning position +// to the current position in the source which effectively ignores the section +// of the source being analyzed. +func (l *lexer) Ignore() { + l.clearRune() + l.startPos = l.currentPos +} + +// Peek performs a Next operation immediately followed by a Rewind returning the +// peeked rune. +func (l *lexer) Peek() rune { + r := l.Next() + l.Rewind() + + return r +} + +// Rewind will take the last rune read (if any) and rewind back. Rewinds can +// occur more than once per call to Next but you can never rewind past the +// last point a token was emitted. +func (l *lexer) Rewind() { + r := l.popRune() + if r > eof { + size := utf8.RuneLen(r) + l.currentPos -= size + if l.currentPos < l.startPos { + l.currentPos = l.startPos + } + } +} + +// Next pulls the next rune from the Lexer and returns it, moving the position +// forward in the source. +func (l *lexer) Next() rune { + str := l.source[l.currentPos:] + if str == "" { + l.pushRune(eof) + return eof + } + + r, size := utf8.DecodeRuneInString(str) + l.currentPos += size + l.pushRune(r) + + return r +} + +// Take receives a string containing all acceptable strings and will contine +// over each consecutive character in the source until a token not in the given +// string is encountered. This should be used to quickly pull token parts. +func (l *lexer) Take(chars string) { + r := l.Next() + for strings.ContainsRune(chars, r) { + r = l.Next() + } + l.Rewind() // last next wasn't a match +} + +// TakeNext is similar to Take but takes if next rune matches +func (l *lexer) TakeNext(ch rune) { + r := l.Next() + + if ch != r { + l.Rewind() // last next wasn't a match + } +} + +func (l *lexer) pushRune(r rune) { + l.runeStack = append(l.runeStack, r) +} + +func (l *lexer) popRune() rune { + r := l.runeStack[len(l.runeStack)-1] + l.runeStack = l.runeStack[:len(l.runeStack)-1] + return r +} + +func (l *lexer) clearRune() { + l.runeStack = l.runeStack[:0] +} diff --git a/lexer_state.go b/lexer_state.go new file mode 100644 index 0000000..76c29f1 --- /dev/null +++ b/lexer_state.go @@ -0,0 +1,351 @@ +package parser + +import ( + "errors" + "fmt" + "unicode" +) + +const ( + breakingTokenSpace = "BREAKING CHANGE" + breakingTokenHyphen = "BREAKING-CHANGE" +) + +var ( + errMissingScopeOrDesc = errors.New("header: missing scope or description") + errScopeMissingParen = errors.New("scope should end with ')'") + errScopeEmpty = errors.New("scope is empty") + errDescMissingDelimiter = errors.New("scope must be followed by ': '") + errHeaderMissingEmptyLine = errors.New("at least one empty line required after header") + errBodyEmptyLine = errors.New("at least one empty line required after body") + + errScopeInvalidChar = "scope: invalid character '%c'" + errTypeInvalidChar = "type: invalid character '%c'" +) + +// all lexer token types that are emitted. +const ( + _ tokenType = iota + + headerTypeToken + headerScopeToken + + leftScopeDelimiterToken + rightScopeDelimiterToken + breakingChangeToken + + descDelimiterToken + descriptionToken + + bodyToken + + footerDelimterToken + footerKeyToken + footerValueToken +) + +func typeState(l *lexer) stateFunc { + for { + r := l.Peek() + + if r == eof { + l.Error(errMissingScopeOrDesc) + return nil + } + + if r == ':' || r == '!' { + l.Emit(headerTypeToken) + return descriptionDelimiterState + } + + if r == '(' { + l.Emit(headerTypeToken) + l.TakeNext('(') + l.Emit(leftScopeDelimiterToken) + return scopeState + } + + if !isValidTypeChar(r) { + l.Error(fmt.Errorf(errTypeInvalidChar, r)) + return nil + } + + l.Next() + } +} + +func scopeState(l *lexer) stateFunc { + for { + r := l.Peek() + + if r == eof { + l.Error(errScopeMissingParen) + return nil + } + + if r == ')' { + if l.Current() == "" { + l.Error(errScopeEmpty) + return nil + } + + l.Emit(headerScopeToken) + l.TakeNext(')') + l.Emit(rightScopeDelimiterToken) + + return descriptionDelimiterState + } + + if !isValidScopeChar(r) { + l.Error(fmt.Errorf(errScopeInvalidChar, r)) + return nil + } + + l.Next() + } +} + +func descriptionDelimiterState(l *lexer) stateFunc { + if l.Peek() == '!' { + l.Next() + l.Emit(breakingChangeToken) + } + + l.Next() + + if l.Current() != ":" || l.Peek() != ' ' { + l.Error(errDescMissingDelimiter) + return nil + } + l.Next() + + l.Emit(descDelimiterToken) + + return descriptionState +} + +func descriptionState(l *lexer) stateFunc { + for { + r := l.Peek() + + if r == eof { + l.Emit(descriptionToken) + return nil + } + + if r == '\n' { + l.Emit(descriptionToken) + return headerDelimeterState + } + + l.Next() + } +} + +func headerDelimeterState(l *lexer) stateFunc { + l.Take("\n") + + if len(l.Current()) < 2 { + l.Error(errHeaderMissingEmptyLine) + return nil + } + + l.Ignore() + + return bodyOrFooterState +} + +func bodyOrFooterState(l *lexer) stateFunc { + count, isFooter := checkIfFooterToken(l) + + // there is no body + if isFooter { + rewind(l, count) + return footerTokenState + } + + return bodyState +} + +func bodyState(l *lexer) stateFunc { + found := takeUntilFirstFooterToken(l) + if !found { + l.Emit(bodyToken) + return nil + } + + // go back to the last newline character + for { + l.Rewind() + + if l.Peek() != '\n' { + break + } + } + l.Next() + l.Emit(bodyToken) + + return bodyDelimiterState +} + +func bodyDelimiterState(l *lexer) stateFunc { + l.Take("\n") + + if len(l.Current()) < 2 { + l.Error(errBodyEmptyLine) + return nil + } + + l.Ignore() + + return footerTokenState +} + +func footerTokenState(l *lexer) stateFunc { + l.Take("\n") + l.Ignore() + + checkIfFooterToken(l) + l.Emit(footerKeyToken) + + return footerDelimiterState +} + +func footerValueState(l *lexer) stateFunc { + if l.Peek() == eof { + return nil + } + + found := takeUntilFirstFooterToken(l) + l.Emit(footerValueToken) + + if !found { + return nil + } + + return footerTokenState +} + +func footerDelimiterState(l *lexer) stateFunc { + l.Take(": #") + l.Emit(footerDelimterToken) + + return footerValueState +} + +// takeUntilFirstFooter takes all characters until a footer token is detected +func takeUntilFirstFooterToken(l *lexer) bool { + for { + r := l.Next() + + if r == eof { + return false + } + + // a footer token has to begin at the start of a line + if r == '\n' { + count, isFooter := checkIfFooterToken(l) + if isFooter { + // if count is > 0 we are at the end of the footer token + // for i := 0; i < count; i++ { + // l.Rewind() + // } + rewind(l, count) + + return true + } + } + } +} + +func rewind(l *lexer, count int) { + for i := count; i > 0; i-- { + l.Rewind() + } +} + +// takeFooterToken continues over each consecutive character in the source +// until an invalid footer token character is detected. The method returns +// the length if there is a valid footer token found. If it is not a valid footer +// token, 0 is returned. + +// token: "BREAKING CHANGE" | "BREAKING-CHANGE" | + +func checkIfFooterToken(l *lexer) (int, bool) { + // handle BREAKING CHANGE + if l.Peek() == 'B' { + // BREAKING-CHANGE: or BREAKING CHANGE: + candidate := peekString(l, len(breakingTokenSpace)+2) + if candidate == breakingTokenSpace+": " || candidate == breakingTokenHyphen+": " { + l.Emit(breakingChangeToken) + l.Take(breakingTokenSpace + "-") + return len(breakingTokenSpace), true + } + } + + count := 0 + r := l.Next() + + for unicode.IsLetter(r) || unicode.IsDigit(r) || r == '-' { + count++ + r = l.Next() + } + + nextChar := l.Peek() + + // : or # delimiter + if (r == ':' && nextChar == ' ') || (r == ' ' && nextChar == '#') { + + l.Rewind() + return count, true + } + + l.Rewind() + + return 0, false +} + +func peekString(l *lexer, count int) string { + s := "" + i := 0 + + defer func() { + for j := 0; j < i; j++ { + l.Rewind() + } + }() + + for i = 0; i < count; i++ { + if l.Peek() == eof { + return s + } + s += string(l.Next()) + } + + return s +} + +// from https://github.com/conventional-commits/parser#the-grammar +//
::= , "(", , ")", ["!"], ":", *, , ["!"], ":", *, + +// ::= + +func isValidTypeChar(r rune) bool { + switch r { + case '\n', '(', ')', ':': + return false + default: + if unicode.IsSpace(r) { + return false + } + return true + } +} + +// ::= + +func isValidScopeChar(r rune) bool { + switch r { + case '\n', '(', ')': + return false + default: + return true + } +} diff --git a/main.go b/main.go deleted file mode 100644 index da0faa8..0000000 --- a/main.go +++ /dev/null @@ -1,19 +0,0 @@ -package main - -import ( - "os" - - "github.com/xfxdev/xlog" -) - -// applicationVersion is set by the linker -var applicationVersion = "0.0.0" - -// main is the entrypoint when called from the command line -func main() { - setup() - if err := rootCmd.Execute(); err != nil { - xlog.Error(err) - os.Exit(1) - } -} diff --git a/parser.go b/parser.go new file mode 100644 index 0000000..82484b7 --- /dev/null +++ b/parser.go @@ -0,0 +1,77 @@ +// Package parser provides a parser for conventional commits +package parser + +import ( + "strings" +) + +// Parser represent a conventional commits parser +type Parser struct{} + +// New returns a new Parser instance +func New() *Parser { + return &Parser{} +} + +// Parse parses the conventional commit. If it fails, an error is returned. +func (p *Parser) Parse(input string) (*Commit, error) { + input = strings.TrimSpace(input) + return p.parse(input) +} + +func (p *Parser) parse(input string) (*Commit, error) { + lex := newLexer(input, typeState, func(error) {}) + lex.Start() + + c := &Commit{ + message: input, + } + + footerCount := 0 + + footerStartPos := 0 + footerEndPos := 0 + + for { + t, done := lex.NextToken() + if done { + break + } + + switch t.Type { + case breakingChangeToken: + c.isBreakingChange = true + case headerTypeToken: + c.commitType = t.Value + case headerScopeToken: + c.scope = t.Value + case descriptionToken: + c.description = t.Value + c.header = strings.TrimSpace(lex.Get(0, t.End)) + case bodyToken: + c.body = strings.TrimSpace(t.Value) + case footerKeyToken: + if footerStartPos == 0 { + footerStartPos = t.Start + } + n := Note{ + token: t.Value, + } + c.notes = append(c.notes, n) + case footerValueToken: + c.notes[footerCount].value = strings.TrimSpace(t.Value) + footerCount++ + footerEndPos = t.End + } + } + + if lex.Err() != nil { + return nil, lex.Err() + } + + if footerStartPos != 0 { + c.footer = strings.TrimSpace(lex.Get(footerStartPos, footerEndPos)) + } + + return c, nil +} diff --git a/parser_bench_test.go b/parser_bench_test.go new file mode 100644 index 0000000..5ed0775 --- /dev/null +++ b/parser_bench_test.go @@ -0,0 +1,39 @@ +package parser + +import ( + "testing" +) + +var sampleCommit = `feat(scope): description + +this is first line in body + +this is second line in body + +Ref: #123 +Date: 01-01-2021 +By: John Doe` + +// regex based parser - last version +// BenchmarkParser-4 229952 4875 ns/op 1365 B/op 21 allocs/op + +// lexer based parser +// BenchmarkParser-4 77244 15265 ns/op 6448 B/op 306 allocs/op + +// lexer: remove linked list stack +// BenchmarkParser-4 152005 7352 ns/op 2928 B/op 60 allocs/op + +var dumpRes *Commit + +var p = New() + +func BenchmarkParser(b *testing.B) { + for i := 0; i < b.N; i++ { + r, err := p.Parse(sampleCommit) + if err != nil { + b.Error(err) + return + } + dumpRes = r + } +} diff --git a/parser_header_test.go b/parser_header_test.go new file mode 100644 index 0000000..71757cf --- /dev/null +++ b/parser_header_test.go @@ -0,0 +1,89 @@ +package parser + +import ( + "strconv" + "strings" + "testing" +) + +func TestParseHeaderValid(t *testing.T) { + var validCases = []string{ + `feat: description with name.txt`, + `feat: description with question?`, + `feat: description with numbers 1, 2, 3 and 4?`, + "feat: !@#$%^&*() ??>?///||| /\\", // just characters. why ? + "feat: 123 description \n\n body 1, 2, 3 and 4?", + "feat: ?123 description \n\n body 1, 2, 3 and 4?", + "feat: description with body 1, \n\n2, 3 and 4?", + "feat1234(@scope/scope1,scope2): description, \n\n body 1 2, 3 and 4?", + "1245#feat1234(@scope/scope1,scope2): description, \n\n body 1 2, 3 and 4?", + "feat: description with colon at the end: mid:dle and :start of words", + "feat: description with gitmoji at the end :hammer:", + "feat: description with gitmoji in :hammer: the middle", + "feat: :hammer: description with gitmoji at the start", + "feat(scope): description with colon at the end: mid:dle and :start of words", + "feat(scope): description with gitmoji at the end :hammer:", + "feat(scope): description with gitmoji in :hammer: the middle", + "feat(scope): :hammer: description with gitmoji at the start", + "feat!: description with colon at the end: mid:dle and :start of words", + "feat!: description with gitmoji at the end :hammer:", + "feat!: description with gitmoji in :hammer: the middle", + "feat!: :hammer: description with gitmoji at the start", + "feat(scope)!: description with colon at the end: mid:dle and :start of words", + "feat(scope)!: description with gitmoji at the end :hammer:", + "feat(scope)!: description with gitmoji in :hammer: the middle", + "feat(scope)!: :hammer: description with gitmoji at the start", + `feat: : : A description`, + `feat: :: A description ::`, + } + + p := New() + + for i, validCase := range validCases { + testName := "case#" + strconv.Itoa(i+1) + t.Run(testName, func(innerT *testing.T) { + headerLine := strings.Split(validCase, "\n")[0] + _, err := p.Parse(headerLine) + if err != nil { + innerT.Error("parseHeader failed for", headerLine, err) + return + } + }) + } +} + +func TestParseHeaderInvalid(t *testing.T) { + var invalidCases = []string{ + `feat:() description with name.txt`, + `feat:1 description with name.txt`, + `feat:! description with name.txt`, + `feat:A description with name.txt`, + `feat123:A description with name.txt`, + `feat!:A description with name.txt`, + `feat())!:A description with name1.txt`, + `feat(()!:A description with name2.txt`, + `feat(scope1)!:A description with name.txt`, + `!feat(scope1)!:A description with name.txt`, + `feat(scope))!: A description with name.txt`, + `feat((scope))!: A description with name.txt`, + `feat((scope)!: A description with name.txt`, + `feat((`, + `feat():`, + `feat):`, + `feat:: A description`, + `feat:::: A description`, + } + + p := New() + + for i, invalidCase := range invalidCases { + testName := "case#" + strconv.Itoa(i+1) + t.Run(testName, func(innerT *testing.T) { + headerLine := strings.Split(invalidCase, "\n")[0] + _, err := p.Parse(headerLine) + if err == nil { + innerT.Error("parseHeader passed without error for", headerLine) + } + }) + } +} diff --git a/parser_test.go b/parser_test.go new file mode 100644 index 0000000..6a0e885 --- /dev/null +++ b/parser_test.go @@ -0,0 +1,315 @@ +package parser + +import ( + "os" + "path/filepath" + "strconv" + "strings" + "testing" +) + +const ( + commitBody = `This is a multiline commit body. + +This is the second line` +) + +const ( + commitDescription = "description message" + commitScope = "scope" + commitType = "type" + + testDataDir = "testdata" +) + +var breakingChangeFooter = []Note{ + newNote("BREAKING CHANGE", "reason"), +} + +var commitFooters = []Note{ + newNote("footer", "simple"), + newNote("hash-footer", "123"), +} + +var multiLineFooters = []Note{ + newNote("footer", `multi line footer +message is here`), + newNote("hash-footer", "123"), +} + +func TestParserDescription(t *testing.T) { + expectedCommit := &Commit{ + commitType: commitType, + description: commitDescription, + } + parseMsgAndCompare(t, "description", expectedCommit) +} + +func TestParserDescriptionScope(t *testing.T) { + expectedCommit := &Commit{ + commitType: commitType, + scope: commitScope, + description: commitDescription, + } + parseMsgAndCompare(t, "description_scope", expectedCommit) +} + +func TestParserBreakingChangeDescription(t *testing.T) { + expectedCommit := &Commit{ + commitType: commitType, + description: commitDescription, + isBreakingChange: true, + } + parseMsgAndCompare(t, "breaking_change_description", expectedCommit) +} + +func TestParserBreakingChangeDescriptionScope(t *testing.T) { + expectedCommit := &Commit{ + commitType: commitType, + scope: commitScope, + description: commitDescription, + isBreakingChange: true, + } + parseMsgAndCompare(t, "breaking_change_description_scope", expectedCommit) +} + +func TestParserDescriptionBody(t *testing.T) { + expectedCommit := &Commit{ + commitType: commitType, + description: commitDescription, + body: commitBody, + } + parseMsgAndCompare(t, "description_body", expectedCommit) +} + +func TestParserDescriptionScopeBody(t *testing.T) { + expectedCommit := &Commit{ + commitType: commitType, + scope: commitScope, + description: commitDescription, + body: commitBody, + } + parseMsgAndCompare(t, "description_scope_body", expectedCommit) +} + +func TestParserBreakingChangeDescriptionBody(t *testing.T) { + expectedCommit := &Commit{ + commitType: commitType, + description: commitDescription, + body: commitBody, + isBreakingChange: true, + } + parseMsgAndCompare(t, "breaking_change_description_body", expectedCommit) +} + +func TestParserBreakingChangeDescriptionScopeBody(t *testing.T) { + expectedCommit := &Commit{ + commitType: commitType, + scope: commitScope, + description: commitDescription, + body: commitBody, + isBreakingChange: true, + } + parseMsgAndCompare(t, "breaking_change_description_scope_body", expectedCommit) +} + +func TestParserDescriptionFooters(t *testing.T) { + expectedCommit := &Commit{ + commitType: commitType, + description: commitDescription, + notes: commitFooters, + } + parseMsgAndCompare(t, "description_footers", expectedCommit) +} + +func TestParserDescriptionScopeFooters(t *testing.T) { + expectedCommit := &Commit{ + commitType: commitType, + scope: commitScope, + description: commitDescription, + notes: commitFooters, + } + parseMsgAndCompare(t, "description_scope_footers", expectedCommit) +} + +func TestParserDescriptionBodyFooters(t *testing.T) { + expectedCommit := &Commit{ + commitType: commitType, + description: commitDescription, + body: commitBody, + notes: commitFooters, + } + parseMsgAndCompare(t, "description_body_footers", expectedCommit) +} + +func TestParserDescriptionScopeBodyFooters(t *testing.T) { + expectedCommit := &Commit{ + commitType: commitType, + scope: commitScope, + description: commitDescription, + body: commitBody, + notes: commitFooters, + } + parseMsgAndCompare(t, "description_scope_body_footers", expectedCommit) +} + +func TestParserDescriptionFootersBreakingChange(t *testing.T) { + expectedCommit := &Commit{ + commitType: commitType, + description: commitDescription, + notes: breakingChangeFooter, + isBreakingChange: true, + } + parseMsgAndCompare(t, "description_footers_breaking_change", expectedCommit) +} + +func TestParserBreakingChangeDescriptionFooters(t *testing.T) { + expectedCommit := &Commit{ + isBreakingChange: true, + commitType: commitType, + description: commitDescription, + notes: commitFooters, + } + parseMsgAndCompare(t, "breaking_change_description_footers", expectedCommit) +} + +func TestParserBreakingChangeDescriptionBodyFooters(t *testing.T) { + expectedCommit := &Commit{ + isBreakingChange: true, + commitType: commitType, + description: commitDescription, + body: commitBody, + notes: commitFooters, + } + parseMsgAndCompare(t, "breaking_change_description_body_footers", expectedCommit) +} + +func TestParserBreakingChangeDescriptionScopeFooters(t *testing.T) { + expectedCommit := &Commit{ + commitType: commitType, + scope: commitScope, + description: commitDescription, + notes: commitFooters, + isBreakingChange: true, + } + parseMsgAndCompare(t, "breaking_change_description_scope_footers", expectedCommit) +} + +func TestParserBreakingChangeDescriptionScopeBodyFooters(t *testing.T) { + expectedCommit := &Commit{ + commitType: commitType, + scope: commitScope, + description: commitDescription, + body: commitBody, + notes: commitFooters, + isBreakingChange: true, + } + parseMsgAndCompare(t, "breaking_change_description_scope_body_footers", expectedCommit) +} + +func TestParserFooterMultiLine(t *testing.T) { + expectedCommit := &Commit{ + commitType: commitType, + description: commitDescription, + notes: multiLineFooters, + } + parseMsgAndCompare(t, "footer_multi_line", expectedCommit) +} + +func TestParserErrNoBlankLine(t *testing.T) { + fileName := "err_no_blank_line" + + commitMsg, err := loadCommitMsgFromFile(fileName) + if err != nil { + t.Error(err) + } + + p := New() + _, err = p.Parse(commitMsg) + if err == nil { + t.Errorf("no error: test file %v passed", fileName) + return + } + + if err != errHeaderMissingEmptyLine { + t.Error("error is not NoBlankLineErr error", err) + } +} + +func parseMsgAndCompare(t *testing.T, fileName string, expectedCommit *Commit) { + commitMsg, err := loadCommitMsgFromFile(fileName) + if err != nil { + t.Errorf("Received unexpected error:\n%+v", err) + return + } + + p := New() + actualCommit, err := p.Parse(commitMsg) + if err != nil { + t.Errorf("Received unexpected error:\n%+v", err) + return + } + + if !compareCommit(t, actualCommit, expectedCommit) { + t.Errorf("Commit not equal :\n\tExpected: %v,\n\tActual: %v", expectedCommit, actualCommit) + return + } +} + +// loadCommitMsgFromFile loads a file and returns the entire contents as a string. Any +// leading or trailing whitespace is removed +func loadCommitMsgFromFile(fileName string) (string, error) { + fullPath := filepath.Join(testDataDir, fileName) + out, err := os.ReadFile(fullPath) + if err != nil { + return "", err + } + return strings.TrimSpace(string(out)), nil +} + +func compareCommit(t *testing.T, a, b *Commit) bool { + if a.commitType != b.commitType { + t.Log("Header Type Not Equal") + return false + } + if a.description != b.description { + t.Log("Header Description Not Equal") + return false + } + if a.scope != b.scope { + t.Log("Header Scope Not Equal") + return false + } + + if a.body != b.body { + t.Logf("Body Not Equal, actual: %s, expected: %s", strconv.Quote(a.body), strconv.Quote(b.body)) + return false + } + + if a.isBreakingChange != b.isBreakingChange { + t.Log("BreakingChange Not Equal") + return false + } + + notesA := a.notes + notesB := b.notes + + if len(notesA) != len(notesB) { + t.Log("Footer Notes Not Equal") + return false + } + + for index, aFoot := range notesA { + bFoot := notesB[index] + if aFoot.Token() != bFoot.Token() { + t.Logf("Footer Notes Token Not Equal, ActualToken1: %s, ExpectedToken2: %s", strconv.Quote(aFoot.Token()), strconv.Quote(bFoot.Token())) + return false + } + if aFoot.Value() != bFoot.Value() { + t.Logf("Footer Notes Value Not Equal, ActualValue1: %s, ExpectedValue2: %s", strconv.Quote(aFoot.Value()), strconv.Quote(bFoot.Value())) + return false + } + } + + return true +} diff --git a/specification.go b/specification.go deleted file mode 100644 index 31a0a7c..0000000 --- a/specification.go +++ /dev/null @@ -1,160 +0,0 @@ -package main - -import ( - "fmt" - "regexp" - "strings" -) - -var ( - // DefaultPatchTypes is the default list of commit types that should be treates as a patch change - DefaultPatchTypes = []string{"fix"} -) - -// ConventionalCommit represents a commit that adheres to the conventional commits specification -type ConventionalCommit struct { - Body string `json:"body"` - BreakingChange bool `json:"breaking_change"` - CommitScope string `json:"scope"` - CommitType string `json:"type"` - Description string `json:"description"` - Footers map[string]string `json:"footers"` -} - -// ParseMessages attempts to parse a slice of commit messages to a slice of -// conventional commits. Returns a slice of errors to indicate all errors -// occurred during parsing -func ParseMessages(messages []string) ([]ConventionalCommit, []error) { - commits := []ConventionalCommit{} - errs := []error{} - - for _, m := range messages { - c, err := ParseMessage(m) - if err != nil { - errs = append(errs, err) - continue - } - - commits = append(commits, c) - } - return commits, errs -} - -// ParseMessage attempts to parse a commit message to a conventional commit -func ParseMessage(message string) (ConventionalCommit, error) { - messageLines := strings.Split(strings.TrimRight(message, "\n\t "), "\n") - - commit := ConventionalCommit{ - Footers: make(map[string]string), - } - currKeyValue := "" - currFooterValue := "" - - inFooters := false - for i, line := range messageLines { - switch i { - case 0: - if err := parseHeader(line, &commit); err != nil { - return commit, err - } - case 1: - if line != "" { - return commit, fmt.Errorf("commit description not followed by an empty line") - } - default: - key, value := parseLineAsFooter(line) - - if key != "" && value != "" { - inFooters = true - - // Check if we have previously found a footer. If we have, set the current footer, - // otherwise just record it. - if currKeyValue != "" { - commit.Footers[currKeyValue] = currFooterValue - } - currKeyValue = key - currFooterValue = value - } else { - if inFooters { - currFooterValue = fmt.Sprintf("%s\n%s", currFooterValue, line) - } else { - if commit.Body == "" { - commit.Body = line - } else { - commit.Body = commit.Body + fmt.Sprintf("\n%s", line) - } - } - } - } - } - - // We reached the end of the commit message, so check if we need to record the footers - if inFooters { - commit.Footers[currKeyValue] = currFooterValue - } - - // Remove whitespace in the commit body - commit.Body = strings.TrimSpace(commit.Body) - - // Check if a footer contained a breaking change - for footer := range commit.Footers { - if footer == "BREAKING CHANGE" || footer == "BREAKING-CHANGE" { - commit.BreakingChange = true - break - } - } - - return commit, nil -} - -// parseLineAsFooter attempts to parse the given line as a footer, returning both the key and the value of the header. -// If the line cannot be parsed then both return values will be empty. -func parseLineAsFooter(line string) (key, value string) { - footerRegexp := regexp.MustCompile(`^(?:(BREAKING[- ]CHANGE|(?:[A-Za-z-])+): |((?:[A-Za-z-])+) #)(.+)$`) - matches := footerRegexp.FindStringSubmatch(line) - if len(matches) != 4 { - return "", "" - } - - if matches[1] == "" { - return matches[2], matches[3] - } - return matches[1], matches[3] -} - -// parseHeader attempts to parse the commit description line and set the appropriate values in the the given commit -func parseHeader(header string, commit *ConventionalCommit) error { - headerRegexp := regexp.MustCompile(`^(?P[A-Za-z]+)(?:\((?P[A-Za-z]+)\))?(?P!)?: (?P[\w| ]+)(?:\n\s*\n(?P(?:.|\n)*)(?:\n\s+\n(?P(?:[A-Za-z-]+: (?:.|\n)*)|(?:BREAKING CHANGE: (?:.|\n)*)|(?:[A-Za-z]+ \#(?:.|\n)*)))?)?$`) - - matches := headerRegexp.FindStringSubmatch(header) - if matches == nil { - return fmt.Errorf("unable to parse commit header: %s", header) - } - - names := headerRegexp.SubexpNames() - for i, match := range matches { - switch names[i] { - case "type": - commit.CommitType = match - case "scope": - commit.CommitScope = match - case "description": - commit.Description = match - case "breaking": - commit.BreakingChange = (match == "!") - } - } - - return nil -} - -// typeIsPatch returns true if the type represents a patch increment -func typeIsPatch(t string, patchTypes []string) bool { - upperT := strings.ToUpper(t) - for _, patchType := range patchTypes { - if strings.ToUpper(patchType) == upperT { - return true - } - } - return false -} diff --git a/specification_internal_test.go b/specification_internal_test.go deleted file mode 100644 index bbf8aad..0000000 --- a/specification_internal_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package main - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestTypeIsPatch(t *testing.T) { - expectedTrue := []string{ - "FIX", - "FIx", - "FiX", - "Fix", - "fIX", - "fIx", - "fiX", - "fix", - } - - for _, ex := range expectedTrue { - assert.True(t, typeIsPatch(ex, DefaultPatchTypes)) - } - - expectedFalse := []string{ - "", - "feat", - } - - for _, ex := range expectedFalse { - assert.False(t, typeIsPatch(ex, DefaultPatchTypes)) - } -} diff --git a/specification_test.go b/specification_test.go deleted file mode 100644 index cd00210..0000000 --- a/specification_test.go +++ /dev/null @@ -1,186 +0,0 @@ -package main_test - -import ( - "testing" - - main "github.com/mbamber/ccp" - "github.com/stretchr/testify/assert" -) - -func parseMessageHelper(t *testing.T, dir string, expected main.ConventionalCommit) { - t.Helper() - - commit := loadStringFromFile(t, dir) - out, err := main.ParseMessage(commit) - if assert.NoError(t, err) { - assert.Equal(t, expected, out) - } -} - -func TestParseMessage(t *testing.T) { - const ( - commitBody = `This is a multiline commit body. - -This is the second line` - commitDescription = "description message" - commitScope = "scope" - commitType = "type" - dir = "test_fixtures/example_commits" - ) - - var ( - breakingChangeFooter = map[string]string{ - "BREAKING CHANGE": "reason", - } - commitFooters = map[string]string{ - "footer": "simple", - "hash-footer": "123", - } - emptyFooters = map[string]string{} - ) - - t.Run("description", func(t *testing.T) { - parseMessageHelper(t, dir, main.ConventionalCommit{ - CommitType: commitType, - Description: commitDescription, - Footers: emptyFooters, - }) - }) - t.Run("description scope", func(t *testing.T) { - parseMessageHelper(t, dir, main.ConventionalCommit{ - CommitScope: commitScope, - CommitType: commitType, - Description: commitDescription, - Footers: emptyFooters, - }) - }) - t.Run("breaking change description", func(t *testing.T) { - parseMessageHelper(t, dir, main.ConventionalCommit{ - BreakingChange: true, - CommitType: commitType, - Description: commitDescription, - Footers: emptyFooters, - }) - }) - t.Run("breaking change description scope", func(t *testing.T) { - parseMessageHelper(t, dir, main.ConventionalCommit{ - BreakingChange: true, - CommitScope: commitScope, - CommitType: commitType, - Description: commitDescription, - Footers: emptyFooters, - }) - }) - t.Run("description body", func(t *testing.T) { - parseMessageHelper(t, dir, main.ConventionalCommit{ - Body: commitBody, - CommitType: commitType, - Description: commitDescription, - Footers: emptyFooters, - }) - }) - t.Run("description scope body", func(t *testing.T) { - parseMessageHelper(t, dir, main.ConventionalCommit{ - Body: commitBody, - CommitScope: commitScope, - CommitType: commitType, - Description: commitDescription, - Footers: emptyFooters, - }) - }) - t.Run("breaking change description body", func(t *testing.T) { - parseMessageHelper(t, dir, main.ConventionalCommit{ - Body: commitBody, - BreakingChange: true, - CommitType: commitType, - Description: commitDescription, - Footers: emptyFooters, - }) - }) - t.Run("breaking change description scope body", func(t *testing.T) { - parseMessageHelper(t, dir, main.ConventionalCommit{ - Body: commitBody, - BreakingChange: true, - CommitScope: commitScope, - CommitType: commitType, - Description: commitDescription, - Footers: emptyFooters, - }) - }) - t.Run("description footers", func(t *testing.T) { - parseMessageHelper(t, dir, main.ConventionalCommit{ - CommitType: commitType, - Description: commitDescription, - Footers: commitFooters, - }) - }) - t.Run("description scope footers", func(t *testing.T) { - parseMessageHelper(t, dir, main.ConventionalCommit{ - CommitScope: commitScope, - CommitType: commitType, - Description: commitDescription, - Footers: commitFooters, - }) - }) - t.Run("description body footers", func(t *testing.T) { - parseMessageHelper(t, dir, main.ConventionalCommit{ - Body: commitBody, - CommitType: commitType, - Description: commitDescription, - Footers: commitFooters, - }) - }) - t.Run("description scope body footers", func(t *testing.T) { - parseMessageHelper(t, dir, main.ConventionalCommit{ - Body: commitBody, - CommitScope: commitScope, - CommitType: commitType, - Description: commitDescription, - Footers: commitFooters, - }) - }) - t.Run("breaking change description footers", func(t *testing.T) { - parseMessageHelper(t, dir, main.ConventionalCommit{ - BreakingChange: true, - CommitType: commitType, - Description: commitDescription, - Footers: commitFooters, - }) - }) - t.Run("breaking change description body footers", func(t *testing.T) { - parseMessageHelper(t, dir, main.ConventionalCommit{ - Body: commitBody, - BreakingChange: true, - CommitType: commitType, - Description: commitDescription, - Footers: commitFooters, - }) - }) - t.Run("breaking change description scope footers", func(t *testing.T) { - parseMessageHelper(t, dir, main.ConventionalCommit{ - BreakingChange: true, - CommitScope: commitScope, - CommitType: commitType, - Description: commitDescription, - Footers: commitFooters, - }) - }) - t.Run("breaking change description scope body footers", func(t *testing.T) { - parseMessageHelper(t, dir, main.ConventionalCommit{ - Body: commitBody, - BreakingChange: true, - CommitScope: commitScope, - CommitType: commitType, - Description: commitDescription, - Footers: commitFooters, - }) - }) - t.Run("description footers breaking change", func(t *testing.T) { - parseMessageHelper(t, dir, main.ConventionalCommit{ - BreakingChange: true, - CommitType: commitType, - Description: commitDescription, - Footers: breakingChangeFooter, - }) - }) -} diff --git a/test_fixtures/example_commits/breaking_change_description b/testdata/breaking_change_description similarity index 100% rename from test_fixtures/example_commits/breaking_change_description rename to testdata/breaking_change_description diff --git a/test_fixtures/example_commits/breaking_change_description_body b/testdata/breaking_change_description_body similarity index 100% rename from test_fixtures/example_commits/breaking_change_description_body rename to testdata/breaking_change_description_body diff --git a/test_fixtures/example_commits/breaking_change_description_body_footers b/testdata/breaking_change_description_body_footers similarity index 100% rename from test_fixtures/example_commits/breaking_change_description_body_footers rename to testdata/breaking_change_description_body_footers diff --git a/test_fixtures/example_commits/breaking_change_description_footers b/testdata/breaking_change_description_footers similarity index 100% rename from test_fixtures/example_commits/breaking_change_description_footers rename to testdata/breaking_change_description_footers diff --git a/test_fixtures/example_commits/breaking_change_description_scope b/testdata/breaking_change_description_scope similarity index 100% rename from test_fixtures/example_commits/breaking_change_description_scope rename to testdata/breaking_change_description_scope diff --git a/test_fixtures/example_commits/breaking_change_description_scope_body b/testdata/breaking_change_description_scope_body similarity index 100% rename from test_fixtures/example_commits/breaking_change_description_scope_body rename to testdata/breaking_change_description_scope_body diff --git a/test_fixtures/example_commits/breaking_change_description_scope_body_footers b/testdata/breaking_change_description_scope_body_footers similarity index 100% rename from test_fixtures/example_commits/breaking_change_description_scope_body_footers rename to testdata/breaking_change_description_scope_body_footers diff --git a/test_fixtures/example_commits/breaking_change_description_scope_footers b/testdata/breaking_change_description_scope_footers similarity index 100% rename from test_fixtures/example_commits/breaking_change_description_scope_footers rename to testdata/breaking_change_description_scope_footers diff --git a/test_fixtures/example_commits/description b/testdata/description similarity index 100% rename from test_fixtures/example_commits/description rename to testdata/description diff --git a/test_fixtures/example_commits/description_body b/testdata/description_body similarity index 100% rename from test_fixtures/example_commits/description_body rename to testdata/description_body diff --git a/test_fixtures/example_commits/description_body_footers b/testdata/description_body_footers similarity index 100% rename from test_fixtures/example_commits/description_body_footers rename to testdata/description_body_footers diff --git a/test_fixtures/example_commits/description_footers b/testdata/description_footers similarity index 100% rename from test_fixtures/example_commits/description_footers rename to testdata/description_footers diff --git a/test_fixtures/example_commits/description_footers_breaking_change b/testdata/description_footers_breaking_change similarity index 100% rename from test_fixtures/example_commits/description_footers_breaking_change rename to testdata/description_footers_breaking_change diff --git a/test_fixtures/example_commits/description_scope b/testdata/description_scope similarity index 100% rename from test_fixtures/example_commits/description_scope rename to testdata/description_scope diff --git a/test_fixtures/example_commits/description_scope_body b/testdata/description_scope_body similarity index 100% rename from test_fixtures/example_commits/description_scope_body rename to testdata/description_scope_body diff --git a/test_fixtures/example_commits/description_scope_body_footers b/testdata/description_scope_body_footers similarity index 100% rename from test_fixtures/example_commits/description_scope_body_footers rename to testdata/description_scope_body_footers diff --git a/test_fixtures/example_commits/description_scope_footers b/testdata/description_scope_footers similarity index 100% rename from test_fixtures/example_commits/description_scope_footers rename to testdata/description_scope_footers diff --git a/testdata/err_no_blank_line b/testdata/err_no_blank_line new file mode 100644 index 0000000..1891d7a --- /dev/null +++ b/testdata/err_no_blank_line @@ -0,0 +1,4 @@ +type(scope): description message +This is a multiline commit body. + +This is the second line \ No newline at end of file diff --git a/testdata/footer_multi_line b/testdata/footer_multi_line new file mode 100644 index 0000000..6d6c807 --- /dev/null +++ b/testdata/footer_multi_line @@ -0,0 +1,6 @@ +type: description message + +footer: multi line footer +message is here + +hash-footer #123 diff --git a/version.go b/version.go deleted file mode 100644 index 653c668..0000000 --- a/version.go +++ /dev/null @@ -1,52 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/Masterminds/semver" - "github.com/xfxdev/xlog" -) - -// GetNextVersion returns the next version to use based on the current version -// and a slice of commit messages -func GetNextVersion(currentVersion string, messages, patchTypes []string) (string, error) { - commits, errs := ParseMessages(messages) - for _, err := range errs { - xlog.Warn(err) // Log any errors, but dont actually terminate - } - - return GetNextVersionFromCommits(currentVersion, commits, patchTypes) -} - -// GetNextVersionFromCommits returns the next version to use based on the current -// version and a slice of `ConventionalCommit`s -func GetNextVersionFromCommits(currentVersion string, commits []ConventionalCommit, patchTypes []string) (string, error) { - current, err := semver.NewVersion(currentVersion) - if err != nil { - return "", fmt.Errorf("Unable to parse version %s", currentVersion) - } - - var newVersion semver.Version - for _, commit := range commits { - if commit.BreakingChange { - xlog.Debugf("Commit message with description \"%s\" has resulted in a major version increment", commit.Description) - newVersion = current.IncMajor() - break - } - - if !typeIsPatch(commit.CommitType, patchTypes) { - xlog.Debugf("Commit message with type \"%s\" and description \"%s\" has resulted in a minor version increment", commit.CommitType, commit.Description) - newVersion = current.IncMinor() - break - } - - newVersion = current.IncPatch() - } - - if newVersion.Equal(&semver.Version{}) { - xlog.Warn("No conventional commits parsed so defaulting to minor increment") - newVersion = current.IncMinor() - } - - return newVersion.String(), nil -} diff --git a/version_test.go b/version_test.go deleted file mode 100644 index 5d3efe3..0000000 --- a/version_test.go +++ /dev/null @@ -1,277 +0,0 @@ -package main_test - -import ( - "fmt" - "testing" - - main "github.com/mbamber/ccp" - "github.com/stretchr/testify/assert" -) - -func TestGetNextVersion(t *testing.T) { - const ( - v1 = "1.0.0" - v11 = "1.1.0" - v101 = "1.0.1" - v2 = "2.0.0" - - minor = "feat: commit description" - major = "feat!: commit description" - bad = "invalid commit message" - ) - - var ( - patch = fmt.Sprintf("%s: commit description", main.DefaultPatchTypes[0]) - ) - - cases := map[string]struct { - current string - messages []string - patchTypes []string - expected string - }{ - "single patch change": { - current: v1, - messages: []string{ - patch, - }, - patchTypes: main.DefaultPatchTypes, - expected: v101, - }, - "single minor change": { - current: v1, - messages: []string{ - minor, - }, - patchTypes: main.DefaultPatchTypes, - expected: v11, - }, - "single major change": { - current: v1, - messages: []string{ - major, - }, - patchTypes: main.DefaultPatchTypes, - expected: v2, - }, - "no changes creates minor increment": { - current: v1, - messages: []string{}, - patchTypes: main.DefaultPatchTypes, - expected: v11, - }, - "major change beats minor change": { - current: v1, - messages: []string{ - major, - minor, - }, - patchTypes: main.DefaultPatchTypes, - expected: v2, - }, - "major change beats patch change": { - current: v1, - messages: []string{ - major, - patch, - }, - patchTypes: main.DefaultPatchTypes, - expected: v2, - }, - "minor change beats patch change": { - current: v1, - messages: []string{ - minor, - patch, - }, - patchTypes: main.DefaultPatchTypes, - expected: v11, - }, - "bad message ignored": { - current: v1, - messages: []string{ - bad, - major, - }, - patchTypes: main.DefaultPatchTypes, - expected: v2, - }, - "only bad messages creates minor increment": { - current: v1, - messages: []string{ - bad, - }, - patchTypes: main.DefaultPatchTypes, - expected: v11, - }, - } - - for name, data := range cases { - v, err := main.GetNextVersion(data.current, data.messages, data.patchTypes) - if assert.NoErrorf(t, err, name) { - assert.Equalf(t, data.expected, v, name) - } - } -} - -func TestGetNextVersionError(t *testing.T) { - const ( - v1 = "1.0.0" - ) - - var ( - patch = fmt.Sprintf("%s: commit description", main.DefaultPatchTypes[0]) - ) - - cases := map[string]struct { - current string - messages []string - patchTypes []string - }{ - "invalid current version": { - current: "bad version", - messages: []string{ - patch, - }, - patchTypes: main.DefaultPatchTypes, - }, - } - - for name, data := range cases { - _, err := main.GetNextVersion(data.current, data.messages, data.patchTypes) - assert.Errorf(t, err, name) - } -} - -func TestGetNextVersionFromCommits(t *testing.T) { - const ( - v1 = "1.0.0" - v11 = "1.1.0" - v101 = "1.0.1" - v2 = "2.0.0" - - d = "commit description" - ) - - var ( - patch = main.ConventionalCommit{ - CommitType: main.DefaultPatchTypes[0], - Description: d, - } - minor = main.ConventionalCommit{ - CommitType: "feat", - Description: d, - } - major = main.ConventionalCommit{ - BreakingChange: true, - CommitType: "feat", - Description: d, - } - ) - - cases := map[string]struct { - current string - commits []main.ConventionalCommit - patchTypes []string - expected string - }{ - "single patch change": { - current: v1, - commits: []main.ConventionalCommit{ - patch, - }, - patchTypes: main.DefaultPatchTypes, - expected: v101, - }, - "single minor change": { - current: v1, - commits: []main.ConventionalCommit{ - minor, - }, - patchTypes: main.DefaultPatchTypes, - expected: v11, - }, - "single major change": { - current: v1, - commits: []main.ConventionalCommit{ - major, - }, - patchTypes: main.DefaultPatchTypes, - expected: v2, - }, - "no changes creates minor increment": { - current: v1, - commits: []main.ConventionalCommit{}, - patchTypes: main.DefaultPatchTypes, - expected: v11, - }, - "major change beats minor change": { - current: v1, - commits: []main.ConventionalCommit{ - major, - minor, - }, - patchTypes: main.DefaultPatchTypes, - expected: v2, - }, - "major change beats patch change": { - current: v1, - commits: []main.ConventionalCommit{ - major, - patch, - }, - patchTypes: main.DefaultPatchTypes, - expected: v2, - }, - "minor change beats patch change": { - current: v1, - commits: []main.ConventionalCommit{ - minor, - patch, - }, - patchTypes: main.DefaultPatchTypes, - expected: v11, - }, - } - - for name, data := range cases { - v, err := main.GetNextVersionFromCommits(data.current, data.commits, data.patchTypes) - if assert.NoErrorf(t, err, name) { - assert.Equalf(t, data.expected, v, name) - } - } -} - -func TestGetNextVersionFromCommitsError(t *testing.T) { - const ( - v1 = "1.0.0" - - d = "commit description" - ) - - var ( - patch = main.ConventionalCommit{ - CommitType: main.DefaultPatchTypes[0], - Description: d, - } - ) - - cases := map[string]struct { - current string - commits []main.ConventionalCommit - patchTypes []string - }{ - "invalid current version": { - current: "bad version", - commits: []main.ConventionalCommit{ - patch, - }, - patchTypes: main.DefaultPatchTypes, - }, - } - - for name, data := range cases { - _, err := main.GetNextVersionFromCommits(data.current, data.commits, data.patchTypes) - assert.Errorf(t, err, name) - } -}