Skip to content

Commit

Permalink
Accept git diff as input
Browse files Browse the repository at this point in the history
This commit allows to pipe `git diff` command output into gocovsh to
display only changed portions of changed files. It might be helpful when
actively working on a feature to determine its coverage quickly, while
disregarding the rest of the code.
  • Loading branch information
orlangure committed Jul 9, 2022
1 parent 07456cc commit 9617e28
Show file tree
Hide file tree
Showing 24 changed files with 565 additions and 49 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@
# vendor/

dist/
gocovsh
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/muesli/termenv v0.9.0
github.com/sebdah/goldie/v2 v2.5.3
github.com/stretchr/testify v1.7.0
github.com/waigani/diffparser v0.0.0-20190828052634-7391f219313d
golang.org/x/tools v0.1.8
)

Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,11 @@ github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/waigani/diffparser v0.0.0-20190828052634-7391f219313d h1:xQcF7b7cZLWZG/+7A4G7un1qmEDYHIvId9qxRS1mZMs=
github.com/waigani/diffparser v0.0.0-20190828052634-7391f219313d/go.mod h1:BzSc3WEF8R+lCaP5iGFRxd5kIXy4JKOZAwNe1w0cdc0=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
Expand Down Expand Up @@ -88,5 +91,6 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
152 changes: 136 additions & 16 deletions internal/codeview/code.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/reflow/truncate"
"github.com/orlangure/gocovsh/internal/styles"
)

const (
Expand Down Expand Up @@ -46,8 +47,17 @@ var (
Align(lipgloss.Right).
BorderForeground(lipgloss.Color(lineNumberColor)).
Border(lipgloss.NormalBorder(), false, true, false, false)

blankBlockSeparatorStyle = lipgloss.NewStyle().
MarginTop(1).MarginBottom(1).
Foreground(lipgloss.Color(lineNumberColor))
)

type filteredLines struct {
actualLines []int
contextLines map[int]bool
}

// New creates a new codeview model which is rendered into the provided width
// and height.
func New(width, height int) Model {
Expand All @@ -60,13 +70,14 @@ func New(width, height int) Model {

// Model is the codeview model. Use New to create a new instance.
type Model struct {
viewport viewport.Model
help help.Model
width int
height int
title string
lines []string
showHelp bool
viewport viewport.Model
help help.Model
width int
height int
title string
lines []string
filteredLines filteredLines
showHelp bool
}

// Update is used to update the internal model state based on the external
Expand Down Expand Up @@ -115,6 +126,14 @@ func (m *Model) SetContent(lines []string) {
m.viewport.SetYOffset(0)
}

// SetFilteredLines sets the lines that should be displayed, while all other
// lines are hidden. If not set, everything is displayed.
func (m *Model) SetFilteredLines(filteredLines []int) {
m.filteredLines = contextifyFilteredLines(filteredLines)
m.redrawLines()
m.viewport.SetYOffset(0)
}

func (m *Model) redrawLines() {
content := m.formatLines(m.lines)
m.viewport.SetContent(content)
Expand Down Expand Up @@ -201,25 +220,80 @@ func (m *Model) recalculateSize() {
}

func (m *Model) formatLines(lines []string) string {
lineNumberStyle := lineNumberStylePlaceholder.Copy().Width(len(fmt.Sprintf("%d", len(lines))) + 1)
lineNumberPlaceholder := lineNumberStyle.Render(fmt.Sprintf("%d", 1))
availableWidth := m.width - lipgloss.Width(lineNumberPlaceholder) - lipgloss.Width(ellipsis)
if len(lines) == 0 {
return ""
}

var buf strings.Builder
var (
buf strings.Builder
filterApplied = len(m.filteredLines.actualLines) > 0
numberWidth = len(fmt.Sprintf("%d", len(lines))) + 1
lineNumberStyle = lineNumberStylePlaceholder.Copy().Width(numberWidth)
printSingleLine = m.linePrinter(&buf, lineNumberStyle)
)

if filterApplied {
lastPrintedLine := 0
separator := blankBlockSeparatorStyle.Render(strings.Repeat("─", max(0, m.width)))

for _, thisLineNumber := range m.filteredLines.actualLines {
if thisLineNumber > len(lines) {
break
}

if thisLineNumber-lastPrintedLine > 1 {
buf.WriteString(separator)
buf.WriteString(newLine)
}

drawPlus := false
line := lines[thisLineNumber-1]

if !m.filteredLines.contextLines[thisLineNumber] {
drawPlus = true
}

printSingleLine(line, thisLineNumber, drawPlus)
lastPrintedLine = thisLineNumber
}
} else {
for i, line := range lines {
printSingleLine(line, i+1, false)
}
}

return buf.String()
}

for i, line := range lines {
type linePrinterFunc func(line string, number int, drawPlus bool)

func (m *Model) linePrinter(buf *strings.Builder, lineNumberStyle lipgloss.Style) linePrinterFunc {
filterApplied := len(m.filteredLines.actualLines) > 0
lineNumberPlaceholder := lineNumberStyle.Render("1")
availableWidth := m.width - lipgloss.Width(lineNumberPlaceholder) - lipgloss.Width(ellipsis)
renderedPlus := styles.CoveredLine.Render("+ ")
renderedSpace := styles.NeutralLine.Render(" ")

return func(line string, number int, drawPlus bool) {
line = m.replaceTabsWithSpaces(line)
lineNumber := lineNumberStyle.Render(fmt.Sprintf("%d", i+1))
lineNumber := lineNumberStyle.Render(fmt.Sprintf("%d", number))
prefix := ""

if filterApplied {
if drawPlus {
prefix = renderedPlus
} else {
prefix = renderedSpace
}
}

if lipgloss.Width(line) > availableWidth {
line = truncate.StringWithTail(line, uint(availableWidth), ellipsis)
}

buf.WriteString(lipgloss.JoinHorizontal(lipgloss.Left, lineNumber, line))
buf.WriteString(lipgloss.JoinHorizontal(lipgloss.Left, prefix, lineNumber, line))
buf.WriteString(newLine)
}

return buf.String()
}

func (m *Model) replaceTabsWithSpaces(line string) string {
Expand Down Expand Up @@ -261,3 +335,49 @@ func max(a, b int) int {

return b
}

func contextifyFilteredLines(input []int) filteredLines {
if len(input) == 0 {
return filteredLines{
actualLines: input,
contextLines: map[int]bool{},
}
}

extendedLines := make([]int, 0, len(input)+2)
contextLines := make(map[int]bool, 2)
lastAddedNumber := input[0]

beforeFirst := lastAddedNumber - 1
if beforeFirst > 0 {
extendedLines = append(extendedLines, beforeFirst)
contextLines[beforeFirst] = true
}

for _, lineNumber := range input {
if lineNumber-lastAddedNumber > 1 {
afterLast := lastAddedNumber + 1
beforeThis := lineNumber - 1

extendedLines = append(extendedLines, afterLast)
contextLines[afterLast] = true

if afterLast != beforeThis {
extendedLines = append(extendedLines, beforeThis)
contextLines[beforeThis] = true
}
}

extendedLines = append(extendedLines, lineNumber)
lastAddedNumber = lineNumber
}

lineAfterLast := input[len(input)-1] + 1
extendedLines = append(extendedLines, lineAfterLast)
contextLines[lineAfterLast] = true

return filteredLines{
actualLines: extendedLines,
contextLines: contextLines,
}
}
95 changes: 95 additions & 0 deletions internal/codeview/code_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package codeview

import (
"fmt"
"testing"

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

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

tests := []struct {
input []int
expectedActual []int
expectedContext map[int]bool
}{
{
input: []int{},
expectedActual: []int{},
expectedContext: map[int]bool{},
},
{
input: []int{1, 2},
expectedActual: []int{1, 2, 3},
expectedContext: map[int]bool{3: true},
},
{
input: []int{2, 3},
expectedActual: []int{1, 2, 3, 4},
expectedContext: map[int]bool{1: true, 4: true},
},
{
input: []int{2, 3, 20},
expectedActual: []int{1, 2, 3, 4, 19, 20, 21},
expectedContext: map[int]bool{
1: true, 4: true,
19: true, 21: true,
},
},
{
input: []int{2, 3, 20, 21},
expectedActual: []int{1, 2, 3, 4, 19, 20, 21, 22},
expectedContext: map[int]bool{
1: true, 4: true,
19: true, 22: true,
},
},
{
input: []int{2, 3, 20, 30},
expectedActual: []int{1, 2, 3, 4, 19, 20, 21, 29, 30, 31},
expectedContext: map[int]bool{
1: true, 4: true,
19: true, 21: true,
29: true, 31: true,
},
},
{
input: []int{2, 3, 20, 22},
expectedActual: []int{1, 2, 3, 4, 19, 20, 21, 22, 23},
expectedContext: map[int]bool{
1: true, 4: true,
19: true, 21: true, 23: true,
},
},
{
input: []int{2, 3, 20, 21, 22, 24, 25, 30},
expectedActual: []int{1, 2, 3, 4, 19, 20, 21, 22, 23, 24, 25, 26, 29, 30, 31},
expectedContext: map[int]bool{
1: true, 4: true,
19: true, 23: true,
26: true,
29: true, 31: true,
},
},
}

for _, test := range tests {
t.Run(fmt.Sprintf("%v", test.input), func(t *testing.T) {
output := contextifyFilteredLines(test.input)
require.EqualValues(t, test.expectedActual, output.actualLines)
require.EqualValues(t, test.expectedContext, output.contextLines)
})
}
}

func Range(from, to int) []int {
result := make([]int, 0, to-from)

for i := 0; i <= to; i++ {
result = append(result, i)
}

return result
}
18 changes: 15 additions & 3 deletions internal/gocovshtest/happy_flow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,26 @@ import (

func TestHappyFlow(t *testing.T) {
t.Run("no requested files", func(t *testing.T) {
testHappyFlow(t, "not-requested", nil)
testHappyFlow(t, "not-requested", nil, nil)
})

t.Run("requested files", func(t *testing.T) {
testHappyFlow(t, "requested", []string{"covered.go"})
testHappyFlow(t, "requested", []string{"covered.go"}, nil)
})

t.Run("filtered lines", func(t *testing.T) {
testHappyFlow(
t,
"filtered",
[]string{"covered.go"},
map[string][]int{
"covered.go": {4},
},
)
})
}

func testHappyFlow(t *testing.T, prefix string, requestedFiles []string) {
func testHappyFlow(t *testing.T, prefix string, requestedFiles []string, filteredLines map[string][]int) {
dir := path.Join("testdata", "general", prefix)
g := goldie.New(t, goldie.WithFixtureDir(dir))

Expand All @@ -27,6 +38,7 @@ func testHappyFlow(t *testing.T, prefix string, requestedFiles []string) {
profileFilename: "profile.cover",
codeRoot: "testdata/general",
requestedFiles: requestedFiles,
filteredLines: filteredLines,
}

t.Run("initial setup", func(t *testing.T) {
Expand Down
2 changes: 2 additions & 0 deletions internal/gocovshtest/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type modelTest struct {
profileFilename string
codeRoot string
requestedFiles []string
filteredLines map[string][]int

m *model.Model
}
Expand All @@ -32,6 +33,7 @@ func (t *modelTest) init() tea.Cmd {
model.WithProfileFilename(t.profileFilename),
model.WithCodeRoot(t.codeRoot),
model.WithRequestedFiles(t.requestedFiles),
model.WithFilteredLines(t.filteredLines),
)

initCmd := t.m.Init()
Expand Down
Loading

0 comments on commit 9617e28

Please sign in to comment.