mirror of
https://github.com/neilotoole/sq.git
synced 2024-12-18 13:41:49 +03:00
7c56377b40
* Field alignment
223 lines
4.9 KiB
Go
223 lines
4.9 KiB
Go
// 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
|
|
}
|