Skip to content

Commit

Permalink
Merge pull request #5 from NiclasvanEyk/feature/search-command
Browse files Browse the repository at this point in the history
`search` subcommand
  • Loading branch information
NiclasvanEyk authored Jul 1, 2023
2 parents c0feeff + b10ee2b commit 980dde7
Show file tree
Hide file tree
Showing 12 changed files with 5,192 additions and 25 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@
- Something This is unexpected
- Something really good
- foo bar foo
- Something
- a `monospaced` item
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ An opiniated way of managing changelogs adhering to the [keepachangelog](https:/

<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://niclasvaneyk.github.io/keepac/demo.dark.gif">
<img src="https://niclasvaneyk.github.io/keepac/demo.light.gif">
<img src="https://niclasvaneyk.github.io/keepac/demo.light.gif" loading="lazy">
</picture>

## Installation
Expand Down Expand Up @@ -67,6 +67,17 @@ While quite basic, this command can be used in conjunction with other tools to q

Renders the nearest changelog right inside your terminal using [charmbracelet/glamour](https://github.com/charmbracelet/glamour).

### `changelog search`

Searches for changes matching the given search query.

<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://niclasvaneyk.github.io/keepac/search.dark.gif">
<img src="https://niclasvaneyk.github.io/keepac/search.light.gif" loading="lazy">
</picture>

Matches are displayed with contextual information, such as the version the change was released in and its type of change.

### `changelog edit`

Opens the nearest changelog inside your `$EDITOR`.
Expand All @@ -79,7 +90,7 @@ Adds a new entry to one of your sections in the changelog.

<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://niclasvaneyk.github.io/keepac/insert.dark.gif">
<img src="https://niclasvaneyk.github.io/keepac/insert.light.gif">
<img src="https://niclasvaneyk.github.io/keepac/insert.light.gif" loading="lazy">
</picture>

Now we arrive at the more useful features of keepac.
Expand Down
36 changes: 36 additions & 0 deletions cmd/search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package cmd

import (
"fmt"

clog "github.com/niclasvaneyk/keepac/internal/changelog"
"github.com/spf13/cobra"
)

// searchCmd represents the grep command
var searchCmd = &cobra.Command{
Use: "search [query]",
Aliases: []string{"grep"},
Short: "Searches for strings in the nearest changelog and prints matches within their context",
Long: ``,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
changelog, _, err := clog.ResolveChangelog()
if err != nil {
return err
}

query := args[0]
result := clog.Search(changelog, query)

if result == "" {
fmt.Println("Nothing matched your query!")
}

return clog.Show(result + "\n")
},
}

func init() {
rootCmd.AddCommand(searchCmd)
}
9 changes: 5 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.20
require (
github.com/spf13/cobra v1.7.0
github.com/yuin/goldmark v1.5.4
golang.org/x/crypto v0.10.0
)

require (
Expand All @@ -28,11 +29,11 @@ require (
github.com/rivo/uniseg v0.2.0 // indirect
github.com/sahilm/fuzzy v0.1.0 // indirect
github.com/yuin/goldmark-emoji v1.0.1 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/term v0.9.0 // indirect
golang.org/x/text v0.10.0 // indirect
)

require (
Expand Down
22 changes: 10 additions & 12 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,13 @@ github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
Expand All @@ -54,7 +52,6 @@ github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs
github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
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/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
Expand All @@ -67,33 +64,34 @@ github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRM
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
14 changes: 9 additions & 5 deletions internal/changelog/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,11 @@ type Bounds struct {

type Section struct {
Type ChangeType
Items []string
Items []Item
Bounds Bounds
}

type Item struct {
Bounds Bounds
}

Expand Down Expand Up @@ -212,15 +216,16 @@ func Parse(source []byte) Changelog {
Stop: r.HeadlineBounds.Stop, // This will be incremented later
}
currentRelease = &r
currentReleaseIsNextRelease = string(heading.Text(source)) == "[Unreleased]"
text := string(heading.Text(source))
currentReleaseIsNextRelease = text == "[Unreleased]" || text == "Unreleased"
}

if heading.Level == 3 && entering {
bounds := ComputeBounds(heading)
bounds.Start = bounds.Start - 4 // we subtract the length of "### " to achieve better insertion points
section := Section{
Type: ParseChangeType(string(heading.Text(source))),
Items: make([]string, 0),
Items: make([]Item, 0),
Bounds: bounds,
}
(*currentRelease).Sections = append((*currentRelease).Sections, section)
Expand All @@ -237,8 +242,7 @@ func Parse(source []byte) Changelog {
}

if node.Kind() == ast.KindListItem && currentRelease != nil && len(currentRelease.Sections) > 0 {
item := node.(*ast.ListItem)
change := string(item.Text(source))
change := Item{Bounds: ComputeBounds(node)}
section := &currentRelease.Sections[len(currentRelease.Sections)-1]
section.Items = append(section.Items, change)
}
Expand Down
59 changes: 59 additions & 0 deletions internal/changelog/search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package changelog

import (
"strings"
)

func Search(changelog *Changelog, query string) string {
output := make([]string, 0)

nextRelease := changelog.Releases.Next
if nextRelease != nil {
includedRelease := false
for _, section := range nextRelease.Sections {
includedSection := false
for _, item := range section.Items {
text := changelog.ContentWithin(&item.Bounds)
if strings.Contains(text, query) {
if !includedRelease {
includedRelease = true
output = append(output, "## "+changelog.ContentWithin(&nextRelease.HeadlineBounds))
}

if !includedSection {
includedSection = true
output = append(output, "### "+ChangeTypeLabel(section.Type))
}

output = append(output, "- "+text)
}
}
}
}

for _, release := range changelog.Releases.Past {
includedRelease := false
for _, section := range release.Sections {
includedSection := false
for _, item := range section.Items {
text := changelog.ContentWithin(&item.Bounds)
if strings.Contains(text, query) {
if !includedRelease {
includedRelease = true
output = append(output, "## "+changelog.ContentWithin(&release.HeadlineBounds))
output = append(output, "")
}

if !includedSection {
includedSection = true
output = append(output, "### "+ChangeTypeLabel(section.Type))
output = append(output, "")
}
output = append(output, "- "+text)
}
}
}
}

return strings.Join(output, "\n")
}
36 changes: 36 additions & 0 deletions internal/changelog/search_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package changelog

import "testing"

func TestItCanSearchForItems(t *testing.T) {
changelog := Parse([]byte(`
# Changelog
## [Unreleased]
### Added
- Something cool
- Another thing
### Removed
- Support for Go > 1.0
- Support for Windows
`))

actual := Search(&changelog, "Windows")
expected := `
# Changelog
## [Unreleased]
### Removed
- Support for Windows
`

if actual != expected {
t.Errorf("Expected does not match actual:\n\n%s", actual)
}
}
23 changes: 21 additions & 2 deletions internal/changelog/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,25 @@ package changelog

import (
"fmt"
"os"

"github.com/charmbracelet/glamour"
"golang.org/x/crypto/ssh/terminal"
)

func (changelog *Changelog) ContentWithin(bounds *Bounds) string {
return changelog.source[bounds.Start:bounds.Stop]
}

func Show(contents string) error {
renderer, _ := glamour.NewTermRenderer(
// detect background color and pick either the default dark or light theme
renderer, err := glamour.NewTermRenderer(
glamour.WithAutoStyle(),
glamour.WithEnvironmentConfig(),
glamour.WithWordWrap(getWordWrapLimit()),
)
if err != nil {
return err
}

out, err := renderer.Render(contents)
if err != nil {
Expand All @@ -25,3 +30,17 @@ func Show(contents string) error {
fmt.Print(out)
return nil
}

func getWordWrapLimit() int {
current := int(os.Stdin.Fd())
width, _, err := terminal.GetSize(current)
if err != nil {
return 80
}

if width > 80 {
return 80
}

return width
}
Loading

0 comments on commit 980dde7

Please sign in to comment.