Skip to content

Commit

Permalink
feat: User defined commit message templates (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanlogue authored Aug 27, 2024
2 parents 2533a8a + 1375e37 commit cceddb7
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 38 deletions.
35 changes: 35 additions & 0 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: PR pipeline

on: pull_request

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: setup go
uses: actions/setup-go@v5
with:
go-version: 1.22.1
- name: Install dependencies
run: go get .
- name: build
run: go build -v ./...
test:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- name: setup go
uses: actions/setup-go@v5
with:
go-version: 1.22.1
- name: Install dependencies
run: go get .
- name: test
run: go test -json > meteor-TestResults.json
- name: upload results
uses: actions/upload-artifact@v4
with:
name: meteor-TestResults
path: meteor-TestResults.json
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased
### Added
- user-defined message templates

## [v0.22.0](https://github.com/stefanlogue/meteor/releases/tag/v0.22.0) - 2024-06-05
### Added
Expand Down
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Or grab a binary from [the latest release](https://github.com/stefanlogue/meteor
You can customise the options available by creating a `.meteor.json` file anywhere in the directory tree (at or above the current working directory). The config file closest to the current working directory will be preferred. This enables you to have different configs for different parent directories, such as one for your personal work, one for your actual work, one for open source work etc.
For global configurations you can create a `config.json` file in the `~/.config/meteor/` directory.

### Boards

![Demo with boards](demos/demo-with-boards.gif)

The content should be in the following format:
Expand Down Expand Up @@ -61,7 +63,25 @@ If you use boards (Jira etc) but need a way to have commits without one, add the
}
```

And if you want to skip the intro screen to save a keypress, add the following to your config:
### Message Templates
If the default commit message templates aren't exactly what you're looking for, you can provide your own! The syntax can be seen in the defaults below:

```json
{
"messageTemplate": "@type(@scope): @message",
"messageWithTicketTemplate": "@ticket(@scope): <@type> @message"
}
```

`messageTemplate` needs to have:
- `@type`: the conventional commit type i.e. `feat`, `chore` etc.
- `@message`: the commit message
- `(@scope)`: (optional but recommended) the scope of the commit, must be within parentheses

`messageWithTicketTemplate` also additionally takes `@ticket`

### Intro
If you want to skip the intro screen to save a keypress, add the following to your config:
```json
{
"showIntro": false
Expand Down
37 changes: 32 additions & 5 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@ import (
"github.com/stefanlogue/meteor/pkg/config"
)

const defaultCommitTitleCharLimit = 48
const (
defaultCommitTitleCharLimit = 48
defaultMessageTemplate = "{{.Type}}{{if .Scope}}({{.Scope}}){{end}}{{if .IsBreakingChange}}!{{end}}: {{.Message}}"
defaultMessageWithTicketTemplate = "{{.TicketNumber}}{{if .Scope}}({{.Scope}}){{end}}{{if .IsBreakingChange}}!{{end}}: <{{.Type}}> {{.Message}}"
)

// loadConfig loads the config file from the current directory or any parent
func loadConfig(fs afero.Fs) ([]huh.Option[string], []huh.Option[string], []huh.Option[string], bool, int, error) {
func loadConfig(fs afero.Fs) ([]huh.Option[string], []huh.Option[string], []huh.Option[string], bool, int, string, string, error) {
filePath, err := config.FindConfigFile(fs)
if err != nil {
log.Debug("Error finding config file", "error", err)
return config.DefaultPrefixes, nil, nil, true, defaultCommitTitleCharLimit, nil
return config.DefaultPrefixes, nil, nil, true, defaultCommitTitleCharLimit, defaultMessageTemplate, defaultMessageWithTicketTemplate, nil
}

log.Debug("found config file", "path", filePath)
Expand All @@ -25,7 +29,7 @@ func loadConfig(fs afero.Fs) ([]huh.Option[string], []huh.Option[string], []huh.

err = c.LoadFile(filePath)
if err != nil {
return nil, nil, nil, true, defaultCommitTitleCharLimit, fmt.Errorf("error parsing config file: %w", err)
return nil, nil, nil, true, defaultCommitTitleCharLimit, defaultMessageTemplate, defaultMessageWithTicketTemplate, fmt.Errorf("error parsing config file: %w", err)
}

if c.ShowIntro == nil {
Expand All @@ -38,5 +42,28 @@ func loadConfig(fs afero.Fs) ([]huh.Option[string], []huh.Option[string], []huh.
c.CommitTitleCharLimit = &commitTitleCharLimit
}

return c.Prefixes.Options(), c.Coauthors.Options(), c.Boards.Options(), *c.ShowIntro, *c.CommitTitleCharLimit, nil
var messageTemplate, messageWithTicketTemplate string
if c.MessageTemplate == nil {
messageTemplate = defaultMessageTemplate
} else {
messageTemplate, err = config.ConvertTemplate(*c.MessageTemplate)
if err != nil {
log.Error("Error converting message template", "error", err)
messageTemplate = defaultMessageTemplate
}
}
c.MessageTemplate = &messageTemplate

if c.MessageWithTicketTemplate == nil {
messageWithTicketTemplate = defaultMessageWithTicketTemplate
} else {
messageWithTicketTemplate, err = config.ConvertTemplate(*c.MessageWithTicketTemplate)
if err != nil {
log.Error("Error converting message with ticket template", "error", err)
messageWithTicketTemplate = defaultMessageWithTicketTemplate
}
}
c.MessageWithTicketTemplate = &messageWithTicketTemplate

return c.Prefixes.Options(), c.Coauthors.Options(), c.Boards.Options(), *c.ShowIntro, *c.CommitTitleCharLimit, messageTemplate, messageWithTicketTemplate, nil
}
38 changes: 11 additions & 27 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"os"
"text/template"
"time"

"github.com/charmbracelet/log"
Expand Down Expand Up @@ -85,7 +86,7 @@ func main() {
fail("Could not change directory: %s", err)
}

prefixes, coauthors, boards, showIntro, commitTitleCharLimit, err := loadConfig(AFS)
prefixes, coauthors, boards, showIntro, commitTitleCharLimit, messageTemplate, messageWithTicketTemplate, err := loadConfig(AFS)
if err != nil {
fail("Error: %s", err)
}
Expand Down Expand Up @@ -183,35 +184,18 @@ func main() {
fail("Error: %s", err)
}

var tmpl *template.Template
if len(newCommit.Board) > 0 && newCommit.Board != "NONE" {
if newCommit.IsBreakingChange {
if len(newCommit.Scope) > 0 {
newCommit.Message = fmt.Sprintf("%s(%s)!: <%s> ", newCommit.TicketNumber, newCommit.Scope, newCommit.Type)
} else {
newCommit.Message = fmt.Sprintf("%s!: <%s> ", newCommit.TicketNumber, newCommit.Type)
}
} else {
if len(newCommit.Scope) > 0 {
newCommit.Message = fmt.Sprintf("%s(%s): <%s> ", newCommit.TicketNumber, newCommit.Scope, newCommit.Type)
} else {
newCommit.Message = fmt.Sprintf("%s: <%s> ", newCommit.TicketNumber, newCommit.Type)
}
}
tmpl = template.Must(template.New("message").Parse(messageWithTicketTemplate))
} else {
if newCommit.IsBreakingChange {
if len(newCommit.Scope) > 0 {
newCommit.Message = fmt.Sprintf("%s(%s)!: ", newCommit.Type, newCommit.Scope)
} else {
newCommit.Message = fmt.Sprintf("%s!: ", newCommit.Type)
}
} else {
if len(newCommit.Scope) > 0 {
newCommit.Message = fmt.Sprintf("%s(%s): ", newCommit.Type, newCommit.Scope)
} else {
newCommit.Message = fmt.Sprintf("%s: ", newCommit.Type)
}
}
tmpl = template.Must(template.New("message").Parse(messageTemplate))
}
buf := new(bytes.Buffer)
err = tmpl.Execute(buf, newCommit)
if err != nil {
fail("Error: %s", err)
}
newCommit.Message = buf.String()

doesWantToCommit := true
messageForm := huh.NewForm(
Expand Down
12 changes: 7 additions & 5 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import (
)

type Config struct {
ShowIntro *bool `json:"showIntro"`
CommitTitleCharLimit *int `json:"commitTitleCharLimit"`
Prefixes Prefixes `json:"prefixes"`
Coauthors CoAuthors `json:"coauthors"`
Boards Boards `json:"boards"`
ShowIntro *bool `json:"showIntro"`
CommitTitleCharLimit *int `json:"commitTitleCharLimit"`
MessageTemplate *string `json:"messageTemplate"`
MessageWithTicketTemplate *string `json:"messageWithTicketTemplate"`
Prefixes Prefixes `json:"prefixes"`
Coauthors CoAuthors `json:"coauthors"`
Boards Boards `json:"boards"`
}

// New returns a new Config
Expand Down
18 changes: 18 additions & 0 deletions pkg/config/messageTemplate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package config

import (
"fmt"
"strings"
)

func ConvertTemplate(t string) (string, error) {
if !strings.Contains(t, "@type") || !strings.Contains(t, "@message") {
return t, fmt.Errorf("template must contain @type and @message")
}
t = strings.Replace(t, ":", "{{if .IsBreakingChange}}!{{end}}:", 1)
t = strings.ReplaceAll(t, "@type", "{{.Type}}")
t = strings.ReplaceAll(t, "(@scope)", "{{if .Scope}}({{.Scope}}){{end}}")
t = strings.ReplaceAll(t, "@ticket", "{{.TicketNumber}}")
t = strings.ReplaceAll(t, "@message", "{{.Message}}")
return t, nil
}
21 changes: 21 additions & 0 deletions pkg/config/messageTemplate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package config

import "testing"

func TestConvertTemplate(t *testing.T) {
cases := []struct {
name string
input string
want string
}{
{"adds breaking change marker", "@type: @message", "{{.Type}}{{if .IsBreakingChange}}!{{end}}: {{.Message}}"},
{"converts template", "@type(@scope): @message", "{{.Type}}{{if .Scope}}({{.Scope}}){{end}}{{if .IsBreakingChange}}!{{end}}: {{.Message}}"},
{"converts without scope", "@type: @message", "{{.Type}}{{if .IsBreakingChange}}!{{end}}: {{.Message}}"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, _ := ConvertTemplate(tc.input)
assertEqual(t, tc.want, got)
})
}
}

0 comments on commit cceddb7

Please sign in to comment.