Skip to content

Commit

Permalink
add extract-changelog (#89)
Browse files Browse the repository at this point in the history
This adds a tool for extracting changelog from the CHANGELOG.md file to
ease up release notes creation.
  • Loading branch information
sywhang authored Sep 18, 2023
1 parent c8f6e38 commit 851bb6a
Show file tree
Hide file tree
Showing 4 changed files with 307 additions and 0 deletions.
140 changes: 140 additions & 0 deletions tools/cmd/extract-changelog/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// extract-changelog extracts the release notes for a specific version from a
// file matching the format prescribed by https://keepachangelog.com/en/1.0.0/.
package main

import (
"bufio"
"bytes"
"errors"
"flag"
"fmt"
"io"
"os"
"strings"
)

func main() {
cmd := mainCmd{
Stdout: os.Stdout,
Stderr: os.Stderr,
}
if err := cmd.Run(os.Args[1:]); err != nil && err != flag.ErrHelp {
fmt.Fprintln(cmd.Stderr, err)
os.Exit(1)
}
}

type mainCmd struct {
Stdout io.Writer
Stderr io.Writer
}

const _usage = `USAGE
%v [OPTIONS] VERSION
Retrieves the release notes for VERSION from a CHANGELOG.md file and prints
them to stdout.
EXAMPLES
extract-changelog -i CHANGELOG.md v1.2.3
extract-changelog 0.2.5
OPTIONS
`

func (cmd *mainCmd) Run(args []string) error {
flag := flag.NewFlagSet("extract-changelog", flag.ContinueOnError)
flag.SetOutput(cmd.Stderr)
flag.Usage = func() {
fmt.Fprintf(flag.Output(), _usage, flag.Name())
flag.PrintDefaults()
}

file := flag.String("i", "CHANGELOG.md", "input file")

if err := flag.Parse(args); err != nil {
return err
}

var version string
if args := flag.Args(); len(args) > 0 {
version = args[0]
}
version = strings.TrimPrefix(version, "v")

if len(version) == 0 {
return errors.New("please provide a version")
}

f, err := os.Open(*file)
if err != nil {
return fmt.Errorf("open changelog: %v", err)
}
defer f.Close()

s, err := extract(f, version)
if err != nil {
return err
}
_, err = io.WriteString(cmd.Stdout, s)
return err
}

func extract(r io.Reader, version string) (string, error) {
type _state int

const (
initial _state = iota
foundHeader
)

var (
state _state
buff bytes.Buffer
scanner = bufio.NewScanner(r)
)

scan:
for scanner.Scan() {
line := scanner.Text()

switch state {
case initial:
// Version headers take one of the following forms:
//
// ## 0.1.3 - 2021-08-18
// ## [0.1.3] - 2021-08-18
switch {
case strings.HasPrefix(line, "## "+version+" "),
strings.HasPrefix(line, "## ["+version+"]"):
fmt.Fprintln(&buff, line)
state = foundHeader
}

case foundHeader:
// Found a new version header. Stop extracting.
if strings.HasPrefix(line, "## ") {
break scan
}
fmt.Fprintln(&buff, line)

default:
// unreachable but guard against it.
return "", fmt.Errorf("unexpected state %v at %q", state, line)
}
}

if err := scanner.Err(); err != nil {
return "", err
}

if state < foundHeader {
return "", fmt.Errorf("changelog for %q not found", version)
}

out := buff.String()
out = strings.TrimSpace(out) + "\n" // always end with a single newline
return out, nil
}
146 changes: 146 additions & 0 deletions tools/cmd/extract-changelog/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package main

import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

const _changelog = `
# Changelog
All notable changes to this project will be documented in this file.
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
- Upcoming feature
## [1.0.0] - 2021-08-18
Initial stable release.
[1.0.0]: http://example.com/1.0.0
## 0.3.0 - 2020-09-01
### Removed
- deprecated functionality
### Fixed
- bug
## [0.2.0] - 2020-08-19
### Added
- Fancy new feature.
[0.2.0]: http://example.com/0.2.0
## 0.1.0 - 2020-08-18
Initial release.
`

func TestMain(t *testing.T) {
t.Parallel()

changelog := filepath.Join(t.TempDir(), "CHANGELOG.md")
require.NoError(t,
os.WriteFile(changelog, []byte(_changelog), 0o644))

tests := []struct {
desc string

version string
want string // expected changelog
wantErr string // expected error, if any
}{
{
desc: "not found",
version: "0.1.1",
wantErr: `changelog for "0.1.1" not found`,
},
{
desc: "missing version",
wantErr: "please provide a version",
},
{
desc: "non-standard body",
version: "1.0.0",
want: joinLines(
"## [1.0.0] - 2021-08-18",
"Initial stable release.",
"",
"[1.0.0]: http://example.com/1.0.0",
),
},
{
desc: "unlinked",
version: "0.3.0",
want: joinLines(
"## 0.3.0 - 2020-09-01",
"### Removed",
"- deprecated functionality",
"",
"### Fixed",
"- bug",
),
},
{
desc: "end of file",
version: "0.1.0",
want: joinLines(
"## 0.1.0 - 2020-08-18",
"",
"Initial release.",
),
},
{
desc: "linked",
version: "0.2.0",
want: joinLines(
"## [0.2.0] - 2020-08-19",
"### Added",
"- Fancy new feature.",
"",
"[0.2.0]: http://example.com/0.2.0",
),
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.desc, func(t *testing.T) {
t.Parallel()

var stdout, stderr bytes.Buffer
defer func() {
assert.Empty(t, stderr.String(), "stderr should be empty")
}()

err := (&mainCmd{
Stdout: &stdout,
Stderr: &stderr,
}).Run([]string{"-i", changelog, tt.version})

if len(tt.wantErr) > 0 {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
return
}

require.NoError(t, err)
assert.Equal(t, tt.want, stdout.String())
})
}
}

// Join a bunch of lines with a trailing newline.
func joinLines(lines ...string) string {
return strings.Join(lines, "\n") + "\n"
}
11 changes: 11 additions & 0 deletions tools/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module github.com/uber-go/mock/tools

go 1.21

require github.com/stretchr/testify v1.8.4

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
10 changes: 10 additions & 0 deletions tools/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 comments on commit 851bb6a

Please sign in to comment.