sq/cli/diff/diff.go

223 lines
4.9 KiB
Go
Raw Normal View History

// Package diff contains the CLI's diff implementation.
//
// Reference:
// - https://github.com/aymanbagabas/go-udiff
// - https://www.gnu.org/software/diffutils/manual/html_node/Hunks.html
// - https://www.cloudbees.com/blog/git-diff-a-complete-comparison-tutorial-for-git
package diff
import (
"context"
"fmt"
"io"
"strings"
udiff "github.com/neilotoole/sq/cli/diff/internal/go-udiff"
"github.com/neilotoole/sq/cli/diff/internal/go-udiff/myers"
"github.com/neilotoole/sq/cli/output"
"github.com/neilotoole/sq/libsq/core/errz"
"github.com/neilotoole/sq/libsq/core/progress"
"github.com/neilotoole/sq/libsq/core/stringz"
"github.com/neilotoole/sq/libsq/source"
"github.com/neilotoole/sq/libsq/source/metadata"
)
// Config contains parameters to control diff behavior.
type Config struct {
// RecordWriterFn is a factory function that returns
// an output.RecordWriter used to generate diff text
// when comparing table data.
RecordWriterFn output.NewRecordWriterFunc
// Lines specifies the number of lines of context surrounding a diff.
Lines int
}
// Elements determines what source elements to compare.
type Elements struct {
// Overview compares a summary of the sources.
Overview bool
// DBProperties compares DB properties.
DBProperties bool
// Schema compares table/schema structure.
Schema bool
// RowCount compares table row count when comparing schemata.
RowCount bool
// Data compares each row in a table. Caution: this can be slow.
Data bool
}
// sourceData encapsulates data about a source.
type sourceData struct {
src *source.Source
srcMeta *metadata.Source
handle string
}
func (sd *sourceData) clone() *sourceData { //nolint:unused // REVISIT: no longer needed?
if sd == nil {
return nil
}
return &sourceData{
handle: sd.handle,
src: sd.src.Clone(),
srcMeta: sd.srcMeta.Clone(),
}
}
// tableData encapsulates data about a table.
type tableData struct {
tblMeta *metadata.Table
src *source.Source
srcMeta *metadata.Source
tblName string
}
func (td *tableData) clone() *tableData { //nolint:unused // REVISIT: no longer needed?
if td == nil {
return nil
}
return &tableData{
tblName: td.tblName,
tblMeta: td.tblMeta.Clone(),
src: td.src.Clone(),
srcMeta: td.srcMeta.Clone(),
}
}
// sourceOverviewDiff is a container for a source overview diff.
type sourceOverviewDiff struct {
sd1, sd2 *sourceData
header string
diff string
}
// tableDiff is a container for a table diff.
type tableDiff struct {
td1, td2 *tableData
header string
diff string
}
// dbPropsDiff is a container for a DB properties diff.
type dbPropsDiff struct {
sd1, sd2 *sourceData
header string
diff string
}
// tableDataDiff is a container for a table's data diff.
type tableDataDiff struct {
td1, td2 *tableData
// recMeta1, recMeta2 record.Meta
header string
diff string
}
// Print prints dif to w. If pr is nil, printing is in monochrome.
func Print(ctx context.Context, w io.Writer, pr *output.Printing, header, dif string) error {
if dif == "" {
return nil
}
if pr == nil || pr.IsMonochrome() {
if header != "" {
dif = header + "\n" + dif
}
_, err := fmt.Fprintln(w, dif)
return errz.Err(err)
}
bar := progress.FromContext(ctx).
NewUnitCounter("Preparing diff output", "line", progress.OptMemUsage)
after := stringz.VisitLines(dif, func(i int, line string) string {
if i == 0 && strings.HasPrefix(line, "---") {
return pr.DiffHeader.Sprint(line)
}
if i == 1 && strings.HasPrefix(line, "+++") {
return pr.DiffHeader.Sprint(line)
}
if strings.HasPrefix(line, "@@") {
return pr.DiffSection.Sprint(line)
}
if strings.HasPrefix(line, "-") {
return pr.DiffMinus.Sprint(line)
}
if strings.HasPrefix(line, "+") {
return pr.DiffPlus.Sprint(line)
}
bar.Incr(1)
return pr.DiffNormal.Sprint(line)
})
if header != "" {
after = pr.DiffHeader.Sprint(header) + "\n" + after
}
bar.Stop()
_, err := fmt.Fprintln(w, after)
return errz.Err(err)
}
// computeUnified encapsulates computing a unified diff.
func computeUnified(ctx context.Context, msg, oldLabel, newLabel string, lines int,
before, after string,
) (string, error) {
if msg == "" {
msg = "Diffing"
} else {
msg = fmt.Sprintf("Diffing (%s)", msg)
}
bar := progress.FromContext(ctx).NewWaiter(msg, true, progress.OptMemUsage)
defer bar.Stop()
var (
unified string
err error
done = make(chan struct{})
)
// We compute the diff on a goroutine because the underlying diff
// library functions aren't context-aware.
go func() {
defer close(done)
edits := myers.ComputeEdits(before, after)
// After edits are computed, if the context is done,
// there's no point continuing.
select {
case <-ctx.Done():
err = errz.Err(ctx.Err())
return
default:
}
unified, err = udiff.ToUnified(
oldLabel,
newLabel,
before,
edits,
lines,
)
}()
select {
case <-ctx.Done():
return "", errz.Err(ctx.Err())
case <-done:
}
return unified, err
}