mirror of
https://github.com/apricote/releaser-pleaser.git
synced 2026-01-13 21:21:03 +00:00
feat(releasepr): release PRs can be updated
- PR Description - Read prefix+suffix from PR description and put into changelog - Keep those overrides on PR description changes - Add pending level to new PRs
This commit is contained in:
parent
cb529f4760
commit
fe871a0213
17 changed files with 1442 additions and 22 deletions
|
|
@ -90,7 +90,7 @@ func UpdateChangelogFile(wt *git.Worktree, newEntry string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewChangelogEntry(changesets []Changeset, version, link string) (string, error) {
|
func NewChangelogEntry(changesets []Changeset, version, link, prefix, suffix string) (string, error) {
|
||||||
features := make([]AnalyzedCommit, 0)
|
features := make([]AnalyzedCommit, 0)
|
||||||
fixes := make([]AnalyzedCommit, 0)
|
fixes := make([]AnalyzedCommit, 0)
|
||||||
|
|
||||||
|
|
@ -111,6 +111,8 @@ func NewChangelogEntry(changesets []Changeset, version, link string) (string, er
|
||||||
"Fixes": fixes,
|
"Fixes": fixes,
|
||||||
"Version": version,
|
"Version": version,
|
||||||
"VersionLink": link,
|
"VersionLink": link,
|
||||||
|
"Prefix": prefix,
|
||||||
|
"Suffix": suffix,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
## [{{.Version}}]({{.VersionLink}})
|
## [{{.Version}}]({{.VersionLink}})
|
||||||
|
{{- if .Prefix }}
|
||||||
|
{{ .Prefix }}
|
||||||
|
{{ end -}}
|
||||||
{{- if (gt (len .Features) 0) }}
|
{{- if (gt (len .Features) 0) }}
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
|
|
@ -13,3 +16,7 @@
|
||||||
- {{ if .Scope }}**{{.Scope}}**: {{end}}{{.Description}}
|
- {{ if .Scope }}**{{.Scope}}**: {{end}}{{.Description}}
|
||||||
{{ end -}}
|
{{ end -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- if .Suffix }}
|
||||||
|
{{ .Suffix }}
|
||||||
|
{{ end -}}
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,8 @@ func Test_NewChangelogEntry(t *testing.T) {
|
||||||
changesets []Changeset
|
changesets []Changeset
|
||||||
version string
|
version string
|
||||||
link string
|
link string
|
||||||
|
prefix string
|
||||||
|
suffix string
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
@ -188,6 +190,54 @@ func Test_NewChangelogEntry(t *testing.T) {
|
||||||
|
|
||||||
- Foobar!
|
- Foobar!
|
||||||
- **sad**: So sad!
|
- **sad**: So sad!
|
||||||
|
`,
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prefix",
|
||||||
|
args: args{
|
||||||
|
changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{
|
||||||
|
{
|
||||||
|
Commit: Commit{},
|
||||||
|
Type: "fix",
|
||||||
|
Description: "Foobar!",
|
||||||
|
},
|
||||||
|
}}},
|
||||||
|
version: "1.0.0",
|
||||||
|
link: "https://example.com/1.0.0",
|
||||||
|
prefix: "### Breaking Changes",
|
||||||
|
},
|
||||||
|
want: `## [1.0.0](https://example.com/1.0.0)
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Foobar!
|
||||||
|
`,
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "suffix",
|
||||||
|
args: args{
|
||||||
|
changesets: []Changeset{{ChangelogEntries: []AnalyzedCommit{
|
||||||
|
{
|
||||||
|
Commit: Commit{},
|
||||||
|
Type: "fix",
|
||||||
|
Description: "Foobar!",
|
||||||
|
},
|
||||||
|
}}},
|
||||||
|
version: "1.0.0",
|
||||||
|
link: "https://example.com/1.0.0",
|
||||||
|
suffix: "### Compatibility\n\nThis version is compatible with flux-compensator v2.2 - v2.9.",
|
||||||
|
},
|
||||||
|
want: `## [1.0.0](https://example.com/1.0.0)
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Foobar!
|
||||||
|
|
||||||
|
### Compatibility
|
||||||
|
|
||||||
|
This version is compatible with flux-compensator v2.2 - v2.9.
|
||||||
`,
|
`,
|
||||||
wantErr: assert.NoError,
|
wantErr: assert.NoError,
|
||||||
},
|
},
|
||||||
|
|
@ -195,7 +245,7 @@ func Test_NewChangelogEntry(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, err := NewChangelogEntry(tt.args.changesets, tt.args.version, tt.args.link)
|
got, err := NewChangelogEntry(tt.args.changesets, tt.args.version, tt.args.link, tt.args.prefix, tt.args.suffix)
|
||||||
if !tt.wantErr(t, err) {
|
if !tt.wantErr(t, err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,7 @@ func reconcileReleasePR(ctx context.Context, forge rp.Forge, changesets []rp.Cha
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
changelogEntry, err := rp.NewChangelogEntry(changesets, nextVersion, forge.ReleaseURL(nextVersion))
|
changelogEntry, err := rp.NewChangelogEntry(changesets, nextVersion, forge.ReleaseURL(nextVersion), releaseOverrides.Prefix, releaseOverrides.Suffix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -237,18 +237,28 @@ func reconcileReleasePR(ctx context.Context, forge rp.Forge, changesets []rp.Cha
|
||||||
|
|
||||||
// Open/Update PR
|
// Open/Update PR
|
||||||
if pr == nil {
|
if pr == nil {
|
||||||
pr = &rp.ReleasePullRequest{
|
pr, err = rp.NewReleasePullRequest(rpBranch, flagBranch, nextVersion, changelogEntry)
|
||||||
Title: releaseCommitMessage,
|
if err != nil {
|
||||||
Description: "TODO",
|
return err
|
||||||
Labels: nil,
|
|
||||||
Head: rpBranch,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pr, err = forge.CreatePullRequest(ctx, pr)
|
err = forge.CreatePullRequest(ctx, pr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
logger.InfoContext(ctx, "opened pull request", "pr.title", pr.Title, "pr.id", pr.ID)
|
logger.InfoContext(ctx, "opened pull request", "pr.title", pr.Title, "pr.id", pr.ID)
|
||||||
|
} else {
|
||||||
|
pr.SetTitle(flagBranch, nextVersion)
|
||||||
|
err = pr.SetDescription(changelogEntry)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = forge.UpdatePullRequest(ctx, pr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.InfoContext(ctx, "updated pull request", "pr.title", pr.Title, "pr.id", pr.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
40
forge.go
40
forge.go
|
|
@ -49,7 +49,8 @@ type Forge interface {
|
||||||
// exists, it returns nil.
|
// exists, it returns nil.
|
||||||
PullRequestForBranch(context.Context, string) (*ReleasePullRequest, error)
|
PullRequestForBranch(context.Context, string) (*ReleasePullRequest, error)
|
||||||
|
|
||||||
CreatePullRequest(context.Context, *ReleasePullRequest) (*ReleasePullRequest, error)
|
CreatePullRequest(context.Context, *ReleasePullRequest) error
|
||||||
|
UpdatePullRequest(context.Context, *ReleasePullRequest) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type ForgeOptions struct {
|
type ForgeOptions struct {
|
||||||
|
|
@ -332,7 +333,7 @@ func (g *GitHub) PullRequestForBranch(ctx context.Context, branch string) (*Rele
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, pr := range prs {
|
for _, pr := range prs {
|
||||||
if pr.GetBase().GetLabel() == g.options.BaseBranch && pr.GetHead().GetLabel() == branch && pr.GetState() == GitHubPRStateOpen {
|
if pr.GetBase().GetRef() == g.options.BaseBranch && pr.GetHead().GetRef() == branch && pr.GetState() == GitHubPRStateOpen {
|
||||||
labels := make([]string, 0, len(pr.Labels))
|
labels := make([]string, 0, len(pr.Labels))
|
||||||
for _, label := range pr.Labels {
|
for _, label := range pr.Labels {
|
||||||
labels = append(labels, label.GetName())
|
labels = append(labels, label.GetName())
|
||||||
|
|
@ -343,7 +344,7 @@ func (g *GitHub) PullRequestForBranch(ctx context.Context, branch string) (*Rele
|
||||||
Title: pr.GetTitle(),
|
Title: pr.GetTitle(),
|
||||||
Description: pr.GetBody(),
|
Description: pr.GetBody(),
|
||||||
Labels: labels,
|
Labels: labels,
|
||||||
Head: pr.GetHead().GetLabel(),
|
Head: pr.GetHead().GetRef(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -357,7 +358,8 @@ func (g *GitHub) PullRequestForBranch(ctx context.Context, branch string) (*Rele
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GitHub) CreatePullRequest(ctx context.Context, pr *ReleasePullRequest) (*ReleasePullRequest, error) {
|
func (g *GitHub) CreatePullRequest(ctx context.Context, pr *ReleasePullRequest) error {
|
||||||
|
// TODO: Labels
|
||||||
ghPR, _, err := g.client.PullRequests.Create(
|
ghPR, _, err := g.client.PullRequests.Create(
|
||||||
ctx, g.options.Owner, g.options.Repo,
|
ctx, g.options.Owner, g.options.Repo,
|
||||||
&github.NewPullRequest{
|
&github.NewPullRequest{
|
||||||
|
|
@ -368,12 +370,36 @@ func (g *GitHub) CreatePullRequest(ctx context.Context, pr *ReleasePullRequest)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
pr.ID = int(*ghPR.ID) // TODO: String ID?
|
_, _, err = g.client.Issues.AddLabelsToIssue(
|
||||||
|
ctx, g.options.Owner, g.options.Repo,
|
||||||
|
ghPR.GetNumber(), pr.Labels,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return pr, nil
|
// TODO: String ID?
|
||||||
|
pr.ID = ghPR.GetNumber()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GitHub) UpdatePullRequest(ctx context.Context, pr *ReleasePullRequest) error {
|
||||||
|
_, _, err := g.client.PullRequests.Edit(
|
||||||
|
ctx, g.options.Owner, g.options.Repo, pr.ID,
|
||||||
|
&github.PullRequest{
|
||||||
|
Title: &pr.Title,
|
||||||
|
Body: &pr.Description,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GitHubOptions) autodiscover() {
|
func (g *GitHubOptions) autodiscover() {
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -10,7 +10,7 @@ require (
|
||||||
github.com/leodido/go-conventionalcommits v0.12.0
|
github.com/leodido/go-conventionalcommits v0.12.0
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/yuin/goldmark v1.7.3
|
github.com/yuin/goldmark v1.7.4
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -90,8 +90,8 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
|
||||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yuin/goldmark v1.7.3 h1:fdk0a/y60GsS4NbEd13GSIP+d8OjtTkmluY32Dy1Z/A=
|
github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
|
||||||
github.com/yuin/goldmark v1.7.3/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
|
|
||||||
32
internal/markdown/extensions/ast/section.go
Normal file
32
internal/markdown/extensions/ast/section.go
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
package ast
|
||||||
|
|
||||||
|
import (
|
||||||
|
gast "github.com/yuin/goldmark/ast"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Section struct represents a section of elements.
|
||||||
|
type Section struct {
|
||||||
|
gast.BaseBlock
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump implements Node.Dump.
|
||||||
|
func (n *Section) Dump(source []byte, level int) {
|
||||||
|
m := map[string]string{
|
||||||
|
"Name": n.Name,
|
||||||
|
}
|
||||||
|
gast.DumpHelper(n, source, level, m, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// KindSection is a NodeKind of the Section node.
|
||||||
|
var KindSection = gast.NewNodeKind("Section")
|
||||||
|
|
||||||
|
// Kind implements Node.Kind.
|
||||||
|
func (n *Section) Kind() gast.NodeKind {
|
||||||
|
return KindSection
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSection returns a new Section node.
|
||||||
|
func NewSection(name string) *Section {
|
||||||
|
return &Section{Name: name}
|
||||||
|
}
|
||||||
88
internal/markdown/extensions/section.go
Normal file
88
internal/markdown/extensions/section.go
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
package extensions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"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/ast"
|
||||||
|
)
|
||||||
|
|
||||||
|
var sectionStartRegex = regexp.MustCompile(`^<!-- section-start (.+) -->`)
|
||||||
|
var sectionEndRegex = regexp.MustCompile(`^<!-- section-end (.+) -->`)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sectionTrigger = "<!--"
|
||||||
|
SectionStartFormat = "<!-- section-start %s -->"
|
||||||
|
SectionEndFormat = "<!-- section-end %s -->"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sectionParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sectionParser) Open(_ gast.Node, reader text.Reader, _ parser.Context) (gast.Node, parser.State) {
|
||||||
|
line, _ := reader.PeekLine()
|
||||||
|
|
||||||
|
if result := sectionStartRegex.FindSubmatch(line); result != nil {
|
||||||
|
reader.AdvanceLine()
|
||||||
|
return ast.NewSection(string(result[1])), parser.HasChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, parser.NoChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sectionParser) Continue(node gast.Node, reader text.Reader, _ parser.Context) parser.State {
|
||||||
|
n := node.(*ast.Section)
|
||||||
|
|
||||||
|
line, _ := reader.PeekLine()
|
||||||
|
|
||||||
|
if result := sectionEndRegex.FindSubmatch(line); result != nil {
|
||||||
|
if string(result[1]) == n.Name {
|
||||||
|
reader.AdvanceLine()
|
||||||
|
return parser.Close
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parser.Continue | parser.HasChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sectionParser) Close(_ gast.Node, _ text.Reader, _ parser.Context) {
|
||||||
|
// Nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sectionParser) CanInterruptParagraph() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sectionParser) CanAcceptIndentedLine() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultSectionParser = §ionParser{}
|
||||||
|
|
||||||
|
// NewSectionParser returns a new BlockParser that can parse
|
||||||
|
// a section block. Section blocks can be used to group various nodes under a parent ast node.
|
||||||
|
// This parser must take precedence over the parser.HTMLParser.
|
||||||
|
func NewSectionParser() parser.BlockParser {
|
||||||
|
return defaultSectionParser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sectionParser) Trigger() []byte {
|
||||||
|
return []byte(sectionTrigger)
|
||||||
|
}
|
||||||
|
|
||||||
|
type section struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section is an extension that allow you to use group content under a shared parent ast node.
|
||||||
|
var Section = §ion{}
|
||||||
|
|
||||||
|
func (e *section) Extend(m goldmark.Markdown) {
|
||||||
|
m.Parser().AddOptions(parser.WithBlockParsers(
|
||||||
|
util.Prioritized(NewSectionParser(), 0),
|
||||||
|
))
|
||||||
|
}
|
||||||
17
internal/markdown/goldmark.go
Normal file
17
internal/markdown/goldmark.go
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
package markdown
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yuin/goldmark"
|
||||||
|
"github.com/yuin/goldmark/renderer"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
|
||||||
|
"github.com/apricote/releaser-pleaser/internal/markdown/extensions"
|
||||||
|
"github.com/apricote/releaser-pleaser/internal/markdown/renderer/markdown"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New() goldmark.Markdown {
|
||||||
|
return goldmark.New(
|
||||||
|
goldmark.WithExtensions(extensions.Section),
|
||||||
|
goldmark.WithRenderer(renderer.NewRenderer(renderer.WithNodeRenderers(util.Prioritized(markdown.NewRenderer(), 1)))),
|
||||||
|
)
|
||||||
|
}
|
||||||
21
internal/markdown/renderer/markdown/LICENSE
Normal file
21
internal/markdown/renderer/markdown/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 Rolf Lewis
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
4
internal/markdown/renderer/markdown/README.md
Normal file
4
internal/markdown/renderer/markdown/README.md
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
This directory is a vendored copy of https://github.com/RolfLewis/goldmark-down/blob/main/markdown.go.
|
||||||
|
The original repository is set to a `main` package which can not be imported.
|
||||||
|
|
||||||
|
It is under the MIT license.
|
||||||
836
internal/markdown/renderer/markdown/renderer.go
Normal file
836
internal/markdown/renderer/markdown/renderer.go
Normal file
|
|
@ -0,0 +1,836 @@
|
||||||
|
package markdown
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
exast "github.com/yuin/goldmark/extension/ast"
|
||||||
|
"github.com/yuin/goldmark/renderer"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
|
||||||
|
rpexast "github.com/apricote/releaser-pleaser/internal/markdown/extensions/ast"
|
||||||
|
)
|
||||||
|
|
||||||
|
type blockState struct {
|
||||||
|
node ast.Node
|
||||||
|
fresh bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type listState struct {
|
||||||
|
marker byte
|
||||||
|
ordered bool
|
||||||
|
index int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Renderer struct {
|
||||||
|
listStack []listState
|
||||||
|
openBlocks []blockState
|
||||||
|
prefixStack []string
|
||||||
|
prefix []byte
|
||||||
|
atNewline bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRenderer returns a new Renderer with given options.
|
||||||
|
func NewRenderer() renderer.NodeRenderer {
|
||||||
|
r := &Renderer{}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||||
|
// default registrations
|
||||||
|
// blocks
|
||||||
|
reg.Register(ast.KindDocument, r.renderDocument)
|
||||||
|
reg.Register(ast.KindHeading, r.renderHeading)
|
||||||
|
reg.Register(ast.KindBlockquote, r.renderBlockquote)
|
||||||
|
reg.Register(ast.KindCodeBlock, r.renderCodeBlock)
|
||||||
|
reg.Register(ast.KindFencedCodeBlock, r.renderFencedCodeBlock)
|
||||||
|
reg.Register(ast.KindHTMLBlock, r.renderHTMLBlock)
|
||||||
|
reg.Register(ast.KindList, r.renderList)
|
||||||
|
reg.Register(ast.KindListItem, r.renderListItem)
|
||||||
|
reg.Register(ast.KindParagraph, r.renderParagraph)
|
||||||
|
reg.Register(ast.KindTextBlock, r.renderTextBlock)
|
||||||
|
reg.Register(ast.KindThematicBreak, r.renderThematicBreak)
|
||||||
|
|
||||||
|
// inlines
|
||||||
|
reg.Register(ast.KindAutoLink, r.renderAutoLink)
|
||||||
|
reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
|
||||||
|
reg.Register(ast.KindEmphasis, r.renderEmphasis)
|
||||||
|
reg.Register(ast.KindImage, r.renderImage)
|
||||||
|
reg.Register(ast.KindLink, r.renderLink)
|
||||||
|
reg.Register(ast.KindRawHTML, r.renderRawHTML)
|
||||||
|
reg.Register(ast.KindText, r.renderText)
|
||||||
|
reg.Register(ast.KindString, r.renderString)
|
||||||
|
|
||||||
|
// GFM Extensions
|
||||||
|
// Tables
|
||||||
|
reg.Register(exast.KindTable, r.renderTable)
|
||||||
|
reg.Register(exast.KindTableHeader, r.renderTableHeader)
|
||||||
|
reg.Register(exast.KindTableRow, r.renderTableRow)
|
||||||
|
reg.Register(exast.KindTableCell, r.renderTableCell)
|
||||||
|
// Strikethrough
|
||||||
|
reg.Register(exast.KindStrikethrough, r.renderStrikethrough)
|
||||||
|
// Checkbox
|
||||||
|
reg.Register(exast.KindTaskCheckBox, r.renderTaskCheckBox)
|
||||||
|
|
||||||
|
// releaser-pleaser Extensions
|
||||||
|
// Section
|
||||||
|
reg.Register(rpexast.KindSection, r.renderSection)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) write(w io.Writer, buf []byte) (int, error) {
|
||||||
|
written := 0
|
||||||
|
for len(buf) > 0 {
|
||||||
|
if r.atNewline {
|
||||||
|
if err := r.beginLine(w); err != nil {
|
||||||
|
return 0, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
atNewline := false
|
||||||
|
newline := bytes.IndexByte(buf, '\n')
|
||||||
|
if newline == -1 {
|
||||||
|
newline = len(buf) - 1
|
||||||
|
} else {
|
||||||
|
atNewline = true
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := w.Write(buf[:newline+1])
|
||||||
|
written += n
|
||||||
|
r.atNewline = n > 0 && atNewline && n == newline+1
|
||||||
|
if len(r.openBlocks) != 0 {
|
||||||
|
r.openBlocks[len(r.openBlocks)-1].fresh = false
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return written, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
buf = buf[n:]
|
||||||
|
}
|
||||||
|
return written, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) beginLine(w io.Writer) error {
|
||||||
|
if len(r.openBlocks) != 0 {
|
||||||
|
current := r.openBlocks[len(r.openBlocks)-1]
|
||||||
|
if current.node.Kind() == ast.KindParagraph && !current.fresh {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := w.Write(r.prefix)
|
||||||
|
if n != 0 {
|
||||||
|
r.atNewline = r.prefix[len(r.prefix)-1] == '\n'
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) writeLines(w util.BufWriter, source []byte, lines *text.Segments) error {
|
||||||
|
for i := 0; i < lines.Len(); i++ {
|
||||||
|
line := lines.At(i)
|
||||||
|
if _, err := r.write(w, line.Value(source)); err != nil {
|
||||||
|
return fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) writeByte(w io.Writer, c byte) error {
|
||||||
|
if _, err := r.write(w, []byte{c}); err != nil {
|
||||||
|
return fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteString writes a string to an io.Writer, ensuring that appropriate indentation and prefices are added at the
|
||||||
|
// beginning of each line.
|
||||||
|
func (r *Renderer) writeString(w io.Writer, s string) (int, error) {
|
||||||
|
n, err := r.write(w, []byte(s))
|
||||||
|
if err != nil {
|
||||||
|
return n, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushIndent adds the specified amount of indentation to the current line prefix.
|
||||||
|
func (r *Renderer) pushIndent(amount int) {
|
||||||
|
r.pushPrefix(strings.Repeat(" ", amount))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushPrefix adds the specified string to the current line prefix.
|
||||||
|
func (r *Renderer) pushPrefix(prefix string) {
|
||||||
|
r.prefixStack = append(r.prefixStack, prefix)
|
||||||
|
r.prefix = append(r.prefix, []byte(prefix)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PopPrefix removes the last piece added by a call to PushIndent or PushPrefix from the current line prefix.
|
||||||
|
func (r *Renderer) popPrefix() {
|
||||||
|
r.prefix = r.prefix[:len(r.prefix)-len(r.prefixStack[len(r.prefixStack)-1])]
|
||||||
|
r.prefixStack = r.prefixStack[:len(r.prefixStack)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenBlock ensures that each block begins on a new line, and that blank lines are inserted before blocks as
|
||||||
|
// indicated by node.HasPreviousBlankLines.
|
||||||
|
func (r *Renderer) openBlock(w util.BufWriter, source []byte, node ast.Node) error {
|
||||||
|
r.openBlocks = append(r.openBlocks, blockState{
|
||||||
|
node: node,
|
||||||
|
fresh: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
hasBlankPreviousLines := node.HasBlankPreviousLines()
|
||||||
|
|
||||||
|
// FIXME: standard goldmark table parser doesn't recognize Blank Previous Lines so we'll always add one
|
||||||
|
if node.Kind() == exast.KindTable {
|
||||||
|
hasBlankPreviousLines = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Work around the fact that the first child of a node notices the same set of preceding blank lines as its parent.
|
||||||
|
if p := node.Parent(); p != nil && p.FirstChild() == node {
|
||||||
|
if p.Kind() == ast.KindDocument || p.Kind() == ast.KindListItem || p.HasBlankPreviousLines() {
|
||||||
|
hasBlankPreviousLines = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasBlankPreviousLines {
|
||||||
|
if err := r.writeByte(w, '\n'); err != nil {
|
||||||
|
return fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.openBlocks[len(r.openBlocks)-1].fresh = true
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseBlock marks the current block as closed.
|
||||||
|
func (r *Renderer) closeBlock(w io.Writer) error {
|
||||||
|
if !r.atNewline {
|
||||||
|
if err := r.writeByte(w, '\n'); err != nil {
|
||||||
|
return fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.openBlocks = r.openBlocks[:len(r.openBlocks)-1]
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderDocument renders an *ast.Document node to the given BufWriter.
|
||||||
|
func (r *Renderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
r.listStack, r.prefixStack, r.prefix, r.atNewline = nil, nil, nil, false
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderHeading renders an *ast.Heading node to the given BufWriter.
|
||||||
|
func (r *Renderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
if enter {
|
||||||
|
if err := r.openBlock(w, source, node); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := r.writeString(w, strings.Repeat("#", node.(*ast.Heading).Level)); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.writeByte(w, ' '); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := r.writeByte(w, '\n'); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.closeBlock(w); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderBlockquote renders an *ast.Blockquote node to the given BufWriter.
|
||||||
|
func (r *Renderer) renderBlockquote(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
if enter {
|
||||||
|
if err := r.openBlock(w, source, node); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := r.writeString(w, "> "); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.pushPrefix("> ")
|
||||||
|
} else {
|
||||||
|
r.popPrefix()
|
||||||
|
|
||||||
|
if err := r.closeBlock(w); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderCodeBlock renders an *ast.CodeBlock node to the given BufWriter.
|
||||||
|
func (r *Renderer) renderCodeBlock(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
if !enter {
|
||||||
|
r.popPrefix()
|
||||||
|
if err := r.closeBlock(w); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.openBlock(w, source, node); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// // Each line of a code block needs to be aligned at the same offset, and a code block must start with at least four
|
||||||
|
// // spaces. To achieve this, we unconditionally add four spaces to the first line of the code block and indent the
|
||||||
|
// // rest as necessary.
|
||||||
|
// if _, err := r.writeString(w, " "); err != nil {
|
||||||
|
// return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
r.pushIndent(4)
|
||||||
|
if err := r.writeLines(w, source, node.Lines()); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderFencedCodeBlock renders an *ast.FencedCodeBlock node to the given BufWriter.
|
||||||
|
func (r *Renderer) renderFencedCodeBlock(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
if !enter {
|
||||||
|
if err := r.closeBlock(w); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.openBlock(w, source, node); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
code := node.(*ast.FencedCodeBlock)
|
||||||
|
|
||||||
|
// Write the start of the fenced code block.
|
||||||
|
fence := []byte("```")
|
||||||
|
if _, err := r.write(w, fence); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
language := code.Language(source)
|
||||||
|
if _, err := r.write(w, language); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
if err := r.writeByte(w, '\n'); err != nil {
|
||||||
|
return ast.WalkStop, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the contents of the fenced code block.
|
||||||
|
if err := r.writeLines(w, source, node.Lines()); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the end of the fenced code block.
|
||||||
|
if err := r.beginLine(w); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
if _, err := r.write(w, fence); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
if err := r.writeByte(w, '\n'); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderHTMLBlock renders an *ast.HTMLBlock node to the given BufWriter.
|
||||||
|
func (r *Renderer) renderHTMLBlock(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
if !enter {
|
||||||
|
if err := r.closeBlock(w); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.openBlock(w, source, node); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the contents of the HTML block.
|
||||||
|
if err := r.writeLines(w, source, node.Lines()); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the closure line, if any.
|
||||||
|
html := node.(*ast.HTMLBlock)
|
||||||
|
if html.HasClosure() {
|
||||||
|
if _, err := r.write(w, html.ClosureLine.Value(source)); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderList renders an *ast.List node to the given BufWriter.
|
||||||
|
func (r *Renderer) renderList(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
if enter {
|
||||||
|
if err := r.openBlock(w, source, node); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
list := node.(*ast.List)
|
||||||
|
r.listStack = append(r.listStack, listState{
|
||||||
|
marker: list.Marker,
|
||||||
|
ordered: list.IsOrdered(),
|
||||||
|
index: list.Start,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
r.listStack = r.listStack[:len(r.listStack)-1]
|
||||||
|
if err := r.closeBlock(w); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderListItem renders an *ast.ListItem node to the given BufWriter.
|
||||||
|
func (r *Renderer) renderListItem(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
if enter {
|
||||||
|
if err := r.openBlock(w, source, node); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
markerWidth := 2 // marker + space
|
||||||
|
|
||||||
|
state := &r.listStack[len(r.listStack)-1]
|
||||||
|
if state.ordered {
|
||||||
|
width, err := r.writeString(w, strconv.FormatInt(int64(state.index), 10))
|
||||||
|
if err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
state.index++
|
||||||
|
markerWidth += width // marker, space, and digits
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := r.write(w, []byte{state.marker, ' '}); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.pushIndent(markerWidth)
|
||||||
|
} else {
|
||||||
|
r.popPrefix()
|
||||||
|
if err := r.closeBlock(w); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderParagraph renders an *ast.Paragraph node to the given BufWriter.
|
||||||
|
func (r *Renderer) renderParagraph(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
if enter {
|
||||||
|
// A paragraph that follows another paragraph or a blockquote must be preceded by a blank line.
|
||||||
|
if !node.HasBlankPreviousLines() {
|
||||||
|
if prev := node.PreviousSibling(); prev != nil && (prev.Kind() == ast.KindParagraph || prev.Kind() == ast.KindBlockquote) {
|
||||||
|
if err := r.writeByte(w, '\n'); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.openBlock(w, source, node); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := r.closeBlock(w); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderTextBlock renders an *ast.TextBlock node to the given BufWriter.
|
||||||
|
func (r *Renderer) renderTextBlock(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
if enter {
|
||||||
|
if err := r.openBlock(w, source, node); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := r.closeBlock(w); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderThematicBreak renders an *ast.ThematicBreak node to the given BufWriter.
|
||||||
|
func (r *Renderer) renderThematicBreak(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
if !enter {
|
||||||
|
if err := r.closeBlock(w); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.openBlock(w, source, node); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this prints an extra no line
|
||||||
|
if _, err := r.writeString(w, "--------"); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderAutoLink renders an *ast.AutoLink node to the given BufWriter.
|
||||||
|
func (r *Renderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
if !enter {
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.writeByte(w, '<'); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
if _, err := r.write(w, node.(*ast.AutoLink).Label(source)); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
if err := r.writeByte(w, '>'); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) shouldPadCodeSpan(source []byte, node *ast.CodeSpan) bool {
|
||||||
|
c := node.FirstChild()
|
||||||
|
if c == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
segment := c.(*ast.Text).Segment
|
||||||
|
text := segment.Value(source)
|
||||||
|
|
||||||
|
var firstChar byte
|
||||||
|
if len(text) > 0 {
|
||||||
|
firstChar = text[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
allWhitespace := true
|
||||||
|
for {
|
||||||
|
if util.FirstNonSpacePosition(text) != -1 {
|
||||||
|
allWhitespace = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
c = c.NextSibling()
|
||||||
|
if c == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
segment = c.(*ast.Text).Segment
|
||||||
|
text = segment.Value(source)
|
||||||
|
}
|
||||||
|
if allWhitespace {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastChar byte
|
||||||
|
if len(text) > 0 {
|
||||||
|
lastChar = text[len(text)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return firstChar == '`' || firstChar == ' ' || lastChar == '`' || lastChar == ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderCodeSpan renders an *ast.CodeSpan node to the given BufWriter.
|
||||||
|
func (r *Renderer) renderCodeSpan(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
if !enter {
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
code := node.(*ast.CodeSpan)
|
||||||
|
delimiter := []byte{'`'}
|
||||||
|
pad := r.shouldPadCodeSpan(source, code)
|
||||||
|
|
||||||
|
if _, err := r.write(w, delimiter); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
if pad {
|
||||||
|
if err := r.writeByte(w, ' '); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for c := node.FirstChild(); c != nil; c = c.NextSibling() {
|
||||||
|
text := c.(*ast.Text).Segment
|
||||||
|
if _, err := r.write(w, text.Value(source)); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pad {
|
||||||
|
if err := r.writeByte(w, ' '); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := r.write(w, delimiter); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast.WalkSkipChildren, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderEmphasis renders an *ast.Emphasis node to the given BufWriter.
|
||||||
|
func (r *Renderer) renderEmphasis(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
em := node.(*ast.Emphasis)
|
||||||
|
if _, err := r.writeString(w, strings.Repeat("*", em.Level)); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) escapeLinkDest(dest []byte) []byte {
|
||||||
|
requiresEscaping := false
|
||||||
|
for _, c := range dest {
|
||||||
|
if c <= 32 || c == '(' || c == ')' || c == 127 {
|
||||||
|
requiresEscaping = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !requiresEscaping {
|
||||||
|
return dest
|
||||||
|
}
|
||||||
|
|
||||||
|
escaped := make([]byte, 0, len(dest)+2)
|
||||||
|
escaped = append(escaped, '<')
|
||||||
|
for _, c := range dest {
|
||||||
|
if c == '<' || c == '>' {
|
||||||
|
escaped = append(escaped, '\\')
|
||||||
|
}
|
||||||
|
escaped = append(escaped, c)
|
||||||
|
}
|
||||||
|
escaped = append(escaped, '>')
|
||||||
|
return escaped
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) linkTitleDelimiter(title []byte) byte {
|
||||||
|
for i, c := range title {
|
||||||
|
if c == '"' && (i == 0 || title[i-1] != '\\') {
|
||||||
|
return '\''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '"'
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) renderLinkOrImage(w util.BufWriter, open string, dest, title []byte, enter bool) error {
|
||||||
|
if enter {
|
||||||
|
if _, err := r.writeString(w, open); err != nil {
|
||||||
|
return fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, err := r.writeString(w, "]("); err != nil {
|
||||||
|
return fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := r.write(w, r.escapeLinkDest(dest)); err != nil {
|
||||||
|
return fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
if len(title) != 0 {
|
||||||
|
delimiter := r.linkTitleDelimiter(title)
|
||||||
|
if _, err := fmt.Fprintf(w, ` %c%s%c`, delimiter, string(title), delimiter); err != nil {
|
||||||
|
return fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.writeByte(w, ')'); err != nil {
|
||||||
|
return fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderImage renders an *ast.Image node to the given BufWriter.
|
||||||
|
func (r *Renderer) renderImage(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
img := node.(*ast.Image)
|
||||||
|
if err := r.renderLinkOrImage(w, "![", img.Destination, img.Title, enter); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderLink renders an *ast.Link node to the given BufWriter.
|
||||||
|
func (r *Renderer) renderLink(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
link := node.(*ast.Link)
|
||||||
|
if err := r.renderLinkOrImage(w, "[", link.Destination, link.Title, enter); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderRawHTML renders an *ast.RawHTML node to the given BufWriter.
|
||||||
|
func (r *Renderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
if !enter {
|
||||||
|
return ast.WalkSkipChildren, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
raw := node.(*ast.RawHTML)
|
||||||
|
for i := 0; i < raw.Segments.Len(); i++ {
|
||||||
|
segment := raw.Segments.At(i)
|
||||||
|
if _, err := r.write(w, segment.Value(source)); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast.WalkSkipChildren, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderText renders an *ast.Text node to the given BufWriter.
|
||||||
|
func (r *Renderer) renderText(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
if !enter {
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
text := node.(*ast.Text)
|
||||||
|
value := text.Segment.Value(source)
|
||||||
|
|
||||||
|
if _, err := r.write(w, value); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case text.HardLineBreak():
|
||||||
|
if _, err := r.writeString(w, "\\\n"); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
case text.SoftLineBreak():
|
||||||
|
if err := r.writeByte(w, '\n'); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderString renders an *ast.String node to the given BufWriter.
|
||||||
|
func (r *Renderer) renderString(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
if !enter {
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
str := node.(*ast.String)
|
||||||
|
if _, err := r.write(w, str.Value); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) renderTable(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
if enter {
|
||||||
|
if err := r.openBlock(w, source, node); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := r.closeBlock(w); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) renderTableHeader(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
if enter {
|
||||||
|
if err := r.openBlock(w, source, node); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := r.writeString(w, "| "); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, err := r.writeString(w, " |\n|"); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for x := 0; x < node.ChildCount(); x++ { // use as column count
|
||||||
|
if _, err := r.writeString(w, " --- |"); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.closeBlock(w); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) renderTableRow(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
if enter {
|
||||||
|
if err := r.openBlock(w, source, node); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := r.writeString(w, "| "); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, err := r.writeString(w, " |"); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.closeBlock(w); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) renderTableCell(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
if !enter {
|
||||||
|
if node.NextSibling() != nil {
|
||||||
|
if _, err := r.writeString(w, " | "); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) renderStrikethrough(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
if _, err := r.writeString(w, "~~"); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
if enter {
|
||||||
|
var fill byte = ' '
|
||||||
|
if task := node.(*exast.TaskCheckBox); task.IsChecked {
|
||||||
|
fill = 'x'
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := r.write(w, []byte{'[', fill, ']', ' '}); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
35
internal/markdown/renderer/markdown/section.go
Normal file
35
internal/markdown/renderer/markdown/section.go
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
package markdown
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
|
||||||
|
"github.com/apricote/releaser-pleaser/internal/markdown/extensions"
|
||||||
|
rpexast "github.com/apricote/releaser-pleaser/internal/markdown/extensions/ast"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *Renderer) renderSection(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||||
|
n := node.(*rpexast.Section)
|
||||||
|
|
||||||
|
if enter {
|
||||||
|
if err := r.openBlock(w, source, node); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := r.writeString(w, fmt.Sprintf(extensions.SectionStartFormat, n.Name)+"\n"); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, err := r.writeString(w, "\n"+fmt.Sprintf(extensions.SectionEndFormat, n.Name)); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.closeBlock(w); err != nil {
|
||||||
|
return ast.WalkStop, fmt.Errorf(": %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
116
releasepr.go
116
releasepr.go
|
|
@ -1,13 +1,34 @@
|
||||||
package rp
|
package rp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
"github.com/yuin/goldmark/ast"
|
"github.com/yuin/goldmark/ast"
|
||||||
"github.com/yuin/goldmark/parser"
|
|
||||||
"github.com/yuin/goldmark/text"
|
"github.com/yuin/goldmark/text"
|
||||||
|
|
||||||
|
"github.com/apricote/releaser-pleaser/internal/markdown"
|
||||||
|
east "github.com/apricote/releaser-pleaser/internal/markdown/extensions/ast"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
releasePRTemplate *template.Template
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed releasepr.md.tpl
|
||||||
|
var rawReleasePRTemplate string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var err error
|
||||||
|
releasePRTemplate, err = template.New("releasepr").Parse(rawReleasePRTemplate)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to parse release pr template: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type ReleasePullRequest struct {
|
type ReleasePullRequest struct {
|
||||||
ID int
|
ID int
|
||||||
Title string
|
Title string
|
||||||
|
|
@ -17,6 +38,20 @@ type ReleasePullRequest struct {
|
||||||
Head string
|
Head string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewReleasePullRequest(head, branch, version, changelogEntry string) (*ReleasePullRequest, error) {
|
||||||
|
rp := &ReleasePullRequest{
|
||||||
|
Head: head,
|
||||||
|
Labels: []string{LabelReleasePending},
|
||||||
|
}
|
||||||
|
|
||||||
|
rp.SetTitle(branch, version)
|
||||||
|
if err := rp.SetDescription(changelogEntry); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rp, nil
|
||||||
|
}
|
||||||
|
|
||||||
type ReleaseOverrides struct {
|
type ReleaseOverrides struct {
|
||||||
Prefix string
|
Prefix string
|
||||||
Suffix string
|
Suffix string
|
||||||
|
|
@ -57,6 +92,9 @@ const (
|
||||||
LabelNextVersionTypeRC = "rp-next-version::rc"
|
LabelNextVersionTypeRC = "rp-next-version::rc"
|
||||||
LabelNextVersionTypeBeta = "rp-next-version::beta"
|
LabelNextVersionTypeBeta = "rp-next-version::beta"
|
||||||
LabelNextVersionTypeAlpha = "rp-next-version::alpha"
|
LabelNextVersionTypeAlpha = "rp-next-version::alpha"
|
||||||
|
|
||||||
|
LabelReleasePending = "rp-release::pending"
|
||||||
|
LabelReleaseTagged = "rp-release::tagged"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -64,6 +102,10 @@ const (
|
||||||
DescriptionLanguageSuffix = "rp-suffix"
|
DescriptionLanguageSuffix = "rp-suffix"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MarkdownSectionOverrides = "overrides"
|
||||||
|
)
|
||||||
|
|
||||||
func (pr *ReleasePullRequest) GetOverrides() (ReleaseOverrides, error) {
|
func (pr *ReleasePullRequest) GetOverrides() (ReleaseOverrides, error) {
|
||||||
overrides := ReleaseOverrides{}
|
overrides := ReleaseOverrides{}
|
||||||
overrides = pr.parseVersioningFlags(overrides)
|
overrides = pr.parseVersioningFlags(overrides)
|
||||||
|
|
@ -95,7 +137,7 @@ func (pr *ReleasePullRequest) parseVersioningFlags(overrides ReleaseOverrides) R
|
||||||
|
|
||||||
func (pr *ReleasePullRequest) parseDescription(overrides ReleaseOverrides) (ReleaseOverrides, error) {
|
func (pr *ReleasePullRequest) parseDescription(overrides ReleaseOverrides) (ReleaseOverrides, error) {
|
||||||
source := []byte(pr.Description)
|
source := []byte(pr.Description)
|
||||||
descriptionAST := parser.NewParser().Parse(text.NewReader(source))
|
descriptionAST := markdown.New().Parser().Parse(text.NewReader(source))
|
||||||
|
|
||||||
err := ast.Walk(descriptionAST, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
|
err := ast.Walk(descriptionAST, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||||
if !entering {
|
if !entering {
|
||||||
|
|
@ -127,6 +169,51 @@ func (pr *ReleasePullRequest) parseDescription(overrides ReleaseOverrides) (Rele
|
||||||
return overrides, nil
|
return overrides, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pr *ReleasePullRequest) getCurrentOverridesText() (string, error) {
|
||||||
|
source := []byte(pr.Description)
|
||||||
|
gm := markdown.New()
|
||||||
|
descriptionAST := gm.Parser().Parse(text.NewReader(source))
|
||||||
|
|
||||||
|
var section *east.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() != east.KindSection {
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
anySection, ok := n.(*east.Section)
|
||||||
|
if !ok {
|
||||||
|
return ast.WalkStop, fmt.Errorf("node has unexpected type: %T", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
if anySection.Name != MarkdownSectionOverrides {
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
section = anySection
|
||||||
|
return ast.WalkStop, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
func textFromLines(source []byte, n ast.Node) string {
|
||||||
content := make([]byte, 0)
|
content := make([]byte, 0)
|
||||||
|
|
||||||
|
|
@ -137,5 +224,28 @@ func textFromLines(source []byte, n ast.Node) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(content)
|
return string(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pr *ReleasePullRequest) SetTitle(branch, version string) {
|
||||||
|
pr.Title = fmt.Sprintf("chore(%s): release %s", branch, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pr *ReleasePullRequest) SetDescription(changelogEntry string) error {
|
||||||
|
overrides, err := pr.getCurrentOverridesText()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var description bytes.Buffer
|
||||||
|
err = releasePRTemplate.Execute(&description, map[string]any{
|
||||||
|
"Changelog": changelogEntry,
|
||||||
|
"Overrides": overrides,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pr.Description = description.String()
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
25
releasepr.md.tpl
Normal file
25
releasepr.md.tpl
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{{ .Changelog }}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## releaser-pleaser Instructions
|
||||||
|
{{ if .Overrides }}
|
||||||
|
{{- .Overrides -}}
|
||||||
|
{{- else }}
|
||||||
|
<!-- section-start overrides -->
|
||||||
|
> If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs.
|
||||||
|
|
||||||
|
### Prefix
|
||||||
|
|
||||||
|
```rp-prefix
|
||||||
|
```
|
||||||
|
|
||||||
|
### Suffix
|
||||||
|
|
||||||
|
```rp-suffix
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- section-end overrides -->
|
||||||
|
|
||||||
|
{{ end }}
|
||||||
|
#### PR by [releaser-pleaser](https://github.com/apricote/releaser-pleaser)
|
||||||
157
releasepr_test.go
Normal file
157
releasepr_test.go
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
package rp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReleasePullRequest_SetTitle(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
branch string
|
||||||
|
version string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
pr *ReleasePullRequest
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple update",
|
||||||
|
pr: &ReleasePullRequest{Title: "foo: bar"},
|
||||||
|
args: args{
|
||||||
|
branch: "main",
|
||||||
|
version: "v1.0.0",
|
||||||
|
},
|
||||||
|
want: "chore(main): release v1.0.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no previous title",
|
||||||
|
pr: &ReleasePullRequest{},
|
||||||
|
args: args{
|
||||||
|
branch: "release-1.x",
|
||||||
|
version: "v1.1.1-rc.0",
|
||||||
|
},
|
||||||
|
want: "chore(release-1.x): release v1.1.1-rc.0",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
tt.pr.SetTitle(tt.args.branch, tt.args.version)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.want, tt.pr.Title)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReleasePullRequest_SetDescription(t *testing.T) {
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
pr *ReleasePullRequest
|
||||||
|
changelogEntry string
|
||||||
|
want string
|
||||||
|
wantErr assert.ErrorAssertionFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty description",
|
||||||
|
pr: &ReleasePullRequest{},
|
||||||
|
changelogEntry: `## v1.0.0`,
|
||||||
|
want: `## v1.0.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## releaser-pleaser Instructions
|
||||||
|
|
||||||
|
<!-- section-start overrides -->
|
||||||
|
> If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs.
|
||||||
|
|
||||||
|
### Prefix
|
||||||
|
|
||||||
|
` + "```" + `rp-prefix
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
### Suffix
|
||||||
|
|
||||||
|
` + "```" + `rp-suffix
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
<!-- section-end overrides -->
|
||||||
|
|
||||||
|
|
||||||
|
#### PR by [releaser-pleaser](https://github.com/apricote/releaser-pleaser)
|
||||||
|
`,
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "existing overrides",
|
||||||
|
pr: &ReleasePullRequest{
|
||||||
|
Description: `## v0.1.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- bedazzle
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## releaser-pleaser Instructions
|
||||||
|
|
||||||
|
<!-- section-start overrides -->
|
||||||
|
> If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs.
|
||||||
|
|
||||||
|
### Prefix
|
||||||
|
|
||||||
|
` + "```" + `rp-prefix
|
||||||
|
This release is awesome!
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
### Suffix
|
||||||
|
|
||||||
|
` + "```" + `rp-suffix
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
<!-- section-end overrides -->
|
||||||
|
|
||||||
|
#### PR by [releaser-pleaser](https://github.com/apricote/releaser-pleaser)
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
changelogEntry: `## v1.0.0`,
|
||||||
|
want: `## v1.0.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## releaser-pleaser Instructions
|
||||||
|
|
||||||
|
<!-- section-start overrides -->
|
||||||
|
> If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs.
|
||||||
|
|
||||||
|
### Prefix
|
||||||
|
|
||||||
|
` + "```" + `rp-prefix
|
||||||
|
This release is awesome!
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
### Suffix
|
||||||
|
|
||||||
|
` + "```" + `rp-suffix
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
<!-- section-end overrides -->
|
||||||
|
|
||||||
|
#### PR by [releaser-pleaser](https://github.com/apricote/releaser-pleaser)
|
||||||
|
`,
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := tt.pr.SetDescription(tt.changelogEntry)
|
||||||
|
if !tt.wantErr(t, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, tt.want, tt.pr.Description)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue