Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User defined commit message templates #40

Merged
merged 5 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
}
Comment on lines +45 to +54

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might ne essier to reader

Suggested change
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
}
}
var messageWithTicketTemplate string
messageTemplate = defaultMessageTemplate
if c.MessageTemplate != nil {
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
}
}
Comment on lines +57 to +65

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could apply the same pattern I described here

#40 (comment)

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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could consider returning a struct, there are way too much returned values

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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your template system might be "hacked" if someone provide a `{{ whatever }}`` syntax

Maybe you could forbid the presence of {{ in the template

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)
})
}
}