Compare commits

..

2 commits

Author SHA1 Message Date
49713e2838
Merge 8c8b8a6e62 into 0750bd6b46 2024-08-31 20:24:09 +00:00
releaser-pleaser
8c8b8a6e62 chore(main): release v0.3.0 2024-08-31 20:24:07 +00:00
6 changed files with 83 additions and 466 deletions

View file

@ -8,7 +8,6 @@ import (
type Section struct {
gast.BaseBlock
Name string
Hidden bool
}
// Dump implements Node.Dump.
@ -27,10 +26,6 @@ func (n *Section) Kind() gast.NodeKind {
return KindSection
}
func (n *Section) HideInOutput() {
n.Hidden = true
}
// NewSection returns a new Section node.
func NewSection(name string) *Section {
return &Section{Name: name}

View file

@ -21,8 +21,8 @@ var (
const (
sectionTrigger = "<!--"
SectionStartFormat = "<!-- section-start %s -->\n"
SectionEndFormat = "\n<!-- section-end %s -->"
SectionStartFormat = "<!-- section-start %s -->"
SectionEndFormat = "<!-- section-end %s -->"
)
type sectionParser struct{}
@ -91,10 +91,6 @@ func (s SectionMarkdownRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegi
func (s SectionMarkdownRenderer) renderSection(w util.BufWriter, _ []byte, node gast.Node, enter bool) (gast.WalkStatus, error) {
n := node.(*ast.Section)
if n.Hidden {
return gast.WalkContinue, nil
}
if enter {
// Add blank previous line if applicable
if node.PreviousSibling() != nil && node.HasBlankPreviousLines() {
@ -111,10 +107,12 @@ func (s SectionMarkdownRenderer) renderSection(w util.BufWriter, _ []byte, node
return gast.WalkStop, fmt.Errorf(": %w", err)
}
if _, err := w.WriteRune('\n'); err != nil {
return gast.WalkStop, err
}
}
// Somehow the goldmark-markdown renderer does not flush this properly on its own
return gast.WalkContinue, w.Flush()
return gast.WalkContinue, nil
}
type section struct{}

View file

@ -2,17 +2,13 @@ package markdown
import (
"bytes"
"strings"
markdown "github.com/teekennedy/goldmark-markdown"
"github.com/yuin/goldmark"
gast "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
"github.com/apricote/releaser-pleaser/internal/markdown/extensions"
"github.com/apricote/releaser-pleaser/internal/markdown/extensions/ast"
)
func New() goldmark.Markdown {
@ -38,82 +34,3 @@ func Format(input string) (string, error) {
return buf.String(), nil
}
func GetCodeBlockText(source []byte, language string, output *string) gast.Walker {
return func(n gast.Node, entering bool) (gast.WalkStatus, error) {
if !entering {
return gast.WalkContinue, nil
}
if n.Kind() != gast.KindFencedCodeBlock {
return gast.WalkContinue, nil
}
codeBlock := n.(*gast.FencedCodeBlock)
if string(codeBlock.Language(source)) != language {
return gast.WalkContinue, nil
}
*output = textFromLines(source, codeBlock)
// Stop looking after we find the first result
return gast.WalkStop, nil
}
}
func GetSectionText(source []byte, name string, output *string) gast.Walker {
return func(n gast.Node, entering bool) (gast.WalkStatus, error) {
if !entering {
return gast.WalkContinue, nil
}
if n.Kind() != ast.KindSection {
return gast.WalkContinue, nil
}
section := n.(*ast.Section)
if section.Name != name {
return gast.WalkContinue, nil
}
// Do not show section markings in output, we only care about the content
section.HideInOutput()
// Found the right section
outputBuffer := new(bytes.Buffer)
err := New().Renderer().Render(outputBuffer, source, section)
if err != nil {
return gast.WalkStop, err
}
*output = outputBuffer.String()
// Stop looking after we find the first result
return gast.WalkStop, nil
}
}
func textFromLines(source []byte, n gast.Node) string {
content := make([]byte, 0)
l := n.Lines().Len()
for i := 0; i < l; i++ {
line := n.Lines().At(i)
content = append(content, line.Value(source)...)
}
return strings.TrimSpace(string(content))
}
func WalkAST(source []byte, walkers ...gast.Walker) (err error) {
doc := New().Parser().Parse(text.NewReader(source))
for _, walker := range walkers {
err = gast.Walk(doc, walker)
if err != nil {
return err
}
}
return nil
}

View file

@ -1,252 +0,0 @@
package markdown
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/yuin/goldmark/ast"
)
func TestFormat(t *testing.T) {
tests := []struct {
name string
input string
want string
wantErr assert.ErrorAssertionFunc
}{
{
name: "heading spacing",
input: "# Foo\n## Bar\n### Baz",
want: "# Foo\n\n## Bar\n\n### Baz\n",
wantErr: assert.NoError,
},
{
name: "no empty lines for list items",
input: "# Foo\n- 1\n- 2\n",
want: "# Foo\n\n- 1\n- 2\n",
wantErr: assert.NoError,
},
{
name: "sections",
input: "# Foo\n<!-- section-start foobar -->\n- 1\n- 2\n<!-- section-end foobar -->\n",
want: "# Foo\n\n<!-- section-start foobar -->\n- 1\n- 2\n\n<!-- section-end foobar -->\n",
wantErr: assert.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Format(tt.input)
if !tt.wantErr(t, err) {
return
}
assert.Equal(t, tt.want, got)
})
}
}
func TestGetCodeBlockText(t *testing.T) {
type args struct {
source []byte
language string
}
tests := []struct {
name string
args args
want string
wantErr assert.ErrorAssertionFunc
}{
{
name: "no code block",
args: args{
source: []byte("# Foo"),
language: "missing",
},
want: "",
wantErr: assert.NoError,
},
{
name: "code block",
args: args{
source: []byte("```test\nContent\n```"),
language: "test",
},
want: "Content",
wantErr: assert.NoError,
},
{
name: "code block with other language",
args: args{
source: []byte("```unknown\nContent\n```"),
language: "test",
},
want: "",
wantErr: assert.NoError,
},
{
name: "multiple code blocks with different languages",
args: args{
source: []byte("```unknown\nContent\n```\n\n```test\n1337\n```"),
language: "test",
},
want: "1337",
wantErr: assert.NoError,
},
{
name: "multiple code blocks with same language returns first one",
args: args{
source: []byte("```test\nContent\n```\n\n```test\n1337\n```"),
language: "test",
},
want: "Content",
wantErr: assert.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got string
err := WalkAST(tt.args.source,
GetCodeBlockText(tt.args.source, tt.args.language, &got),
)
if !tt.wantErr(t, err) {
return
}
assert.Equal(t, tt.want, got)
})
}
}
func TestGetSectionText(t *testing.T) {
type args struct {
source []byte
name string
}
tests := []struct {
name string
args args
want string
wantErr assert.ErrorAssertionFunc
}{
{
name: "no section",
args: args{
source: []byte("# Foo"),
name: "missing",
},
want: "",
wantErr: assert.NoError,
},
{
name: "section",
args: args{
source: []byte("<!-- section-start test -->\nContent\n<!-- section-end test -->"),
name: "test",
},
want: "Content\n",
wantErr: assert.NoError,
},
{
name: "section with other name",
args: args{
source: []byte("<!-- section-start unknown -->\nContent\n<!-- section-end unknown -->"),
name: "test",
},
want: "",
wantErr: assert.NoError,
},
{
name: "multiple sections with different names",
args: args{
source: []byte("<!-- section-start unknown -->\nContent\n<!-- section-end unknown -->\n\n<!-- section-start test -->\n1337\n<!-- section-end test -->"),
name: "test",
},
want: "1337\n",
wantErr: assert.NoError,
},
{
name: "multiple sections with same name returns first one",
args: args{
source: []byte("<!-- section-start test -->\nContent\n<!-- section-end test -->\n\n<!-- section-start test -->\n1337\n<!-- section-end test -->"),
name: "test",
},
want: "Content\n",
wantErr: assert.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got string
err := WalkAST(tt.args.source,
GetSectionText(tt.args.source, tt.args.name, &got),
)
if !tt.wantErr(t, err) {
return
}
assert.Equal(t, tt.want, got)
})
}
}
func TestWalkAST(t *testing.T) {
type args struct {
source []byte
walkers []ast.Walker
}
tests := []struct {
name string
args args
wantErr assert.ErrorAssertionFunc
}{
{
name: "empty walker",
args: args{
source: []byte("# Foo"),
walkers: []ast.Walker{
func(_ ast.Node, _ bool) (ast.WalkStatus, error) {
return ast.WalkStop, nil
},
},
},
wantErr: assert.NoError,
},
{
name: "returns walker error",
args: args{
source: []byte("# Foo"),
walkers: []ast.Walker{
func(_ ast.Node, _ bool) (ast.WalkStatus, error) {
return ast.WalkStop, errors.New("test")
},
},
},
wantErr: assert.Error,
},
{
name: "runs all walkers",
args: args{
source: []byte("# Foo"),
walkers: []ast.Walker{
func(_ ast.Node, _ bool) (ast.WalkStatus, error) {
return ast.WalkStop, nil
},
func(_ ast.Node, _ bool) (ast.WalkStatus, error) {
return ast.WalkStop, errors.New("test")
},
},
},
wantErr: assert.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := WalkAST(tt.args.source, tt.args.walkers...)
if !tt.wantErr(t, err) {
return
}
})
}
}

View file

@ -6,10 +6,15 @@ import (
"fmt"
"log"
"regexp"
"strings"
"text/template"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/text"
"github.com/apricote/releaser-pleaser/internal/git"
"github.com/apricote/releaser-pleaser/internal/markdown"
ast2 "github.com/apricote/releaser-pleaser/internal/markdown/extensions/ast"
"github.com/apricote/releaser-pleaser/internal/versioning"
)
@ -135,11 +140,31 @@ func (pr *ReleasePullRequest) parseVersioningFlags(overrides ReleaseOverrides) R
func (pr *ReleasePullRequest) parseDescription(overrides ReleaseOverrides) (ReleaseOverrides, error) {
source := []byte(pr.Description)
descriptionAST := markdown.New().Parser().Parse(text.NewReader(source))
err := markdown.WalkAST(source,
markdown.GetCodeBlockText(source, DescriptionLanguagePrefix, &overrides.Prefix),
markdown.GetCodeBlockText(source, DescriptionLanguageSuffix, &overrides.Suffix),
)
err := ast.Walk(descriptionAST, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
if n.Type() != ast.TypeBlock || n.Kind() != ast.KindFencedCodeBlock {
return ast.WalkContinue, nil
}
codeBlock, ok := n.(*ast.FencedCodeBlock)
if !ok {
return ast.WalkStop, fmt.Errorf("node has unexpected type: %T", n)
}
switch string(codeBlock.Language(source)) {
case DescriptionLanguagePrefix:
overrides.Prefix = textFromLines(source, codeBlock)
case DescriptionLanguageSuffix:
overrides.Suffix = textFromLines(source, codeBlock)
}
return ast.WalkContinue, nil
})
if err != nil {
return ReleaseOverrides{}, err
}
@ -149,15 +174,59 @@ func (pr *ReleasePullRequest) parseDescription(overrides ReleaseOverrides) (Rele
func (pr *ReleasePullRequest) ChangelogText() (string, error) {
source := []byte(pr.Description)
gm := markdown.New()
descriptionAST := gm.Parser().Parse(text.NewReader(source))
var sectionText string
err := markdown.WalkAST(source, markdown.GetSectionText(source, MarkdownSectionChangelog, &sectionText))
var section *ast2.Section
err := ast.Walk(descriptionAST, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
if n.Type() != ast.TypeBlock || n.Kind() != ast2.KindSection {
return ast.WalkContinue, nil
}
anySection, ok := n.(*ast2.Section)
if !ok {
return ast.WalkStop, fmt.Errorf("node has unexpected type: %T", n)
}
if anySection.Name != MarkdownSectionChangelog {
return ast.WalkContinue, nil
}
section = anySection
return ast.WalkStop, nil
})
if err != nil {
return "", err
}
return sectionText, nil
if section == nil {
return "", nil
}
outputBuffer := new(bytes.Buffer)
err = gm.Renderer().Render(outputBuffer, source, section)
if err != nil {
return "", err
}
return outputBuffer.String(), nil
}
func textFromLines(source []byte, n ast.Node) string {
content := make([]byte, 0)
l := n.Lines().Len()
for i := 0; i < l; i++ {
line := n.Lines().At(i)
content = append(content, line.Value(source)...)
}
return strings.TrimSpace(string(content))
}
func (pr *ReleasePullRequest) SetTitle(branch, version string) {

View file

@ -1,121 +1,11 @@
package releasepr
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/apricote/releaser-pleaser/internal/versioning"
)
func TestReleasePullRequest_GetOverrides(t *testing.T) {
tests := []struct {
name string
pr ReleasePullRequest
want ReleaseOverrides
wantErr assert.ErrorAssertionFunc
}{
{
name: "empty",
pr: ReleasePullRequest{},
want: ReleaseOverrides{},
wantErr: assert.NoError,
},
{
// TODO: Test for multiple version flags
name: "single version flag",
pr: ReleasePullRequest{
Labels: []Label{LabelNextVersionTypeAlpha},
},
want: ReleaseOverrides{
NextVersionType: versioning.NextVersionTypeAlpha,
},
wantErr: assert.NoError,
},
{
name: "prefix in description",
pr: ReleasePullRequest{
Description: "```rp-prefix\n## Foo\n\n- Cool thing\n```",
},
want: ReleaseOverrides{Prefix: "## Foo\n\n- Cool thing"},
wantErr: assert.NoError,
},
{
name: "suffix in description",
pr: ReleasePullRequest{
Description: "```rp-suffix\n## Compatibility\n\nNo compatibility guarantees.\n```",
},
want: ReleaseOverrides{Suffix: "## Compatibility\n\nNo compatibility guarantees."},
wantErr: assert.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.pr.GetOverrides()
if !tt.wantErr(t, err, fmt.Sprintf("GetOverrides()")) {
return
}
assert.Equalf(t, tt.want, got, "GetOverrides()")
})
}
}
func TestReleasePullRequest_ChangelogText(t *testing.T) {
tests := []struct {
name string
description string
want string
wantErr assert.ErrorAssertionFunc
}{
{
name: "no section",
description: "# Foo\n",
want: "",
wantErr: assert.NoError,
},
{
name: "with section",
description: `# Foobar
<!-- section-start changelog -->
This is the changelog
## Awesome
### New
#### Changes
<!-- section-end changelog -->
Suffix Things
`,
want: `This is the changelog
## Awesome
### New
#### Changes
`,
wantErr: assert.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pr := &ReleasePullRequest{
Description: tt.description,
}
got, err := pr.ChangelogText()
if !tt.wantErr(t, err, fmt.Sprintf("ChangelogText()")) {
return
}
assert.Equalf(t, tt.want, got, "ChangelogText()")
})
}
}
func TestReleasePullRequest_SetTitle(t *testing.T) {
type args struct {
branch string