Files
nostrlib/nip34/gitnaturalapi/diff.go
T

414 lines
7.4 KiB
Go

package gitnaturalapi
import (
"strings"
"sync"
)
type DiffLine struct {
Index int
Status string
Text string
Change string
}
type DiffFile struct {
Path string
Status string
Content []byte
Lines []DiffLine
}
type changedEntry struct {
newVersion TreeEntry
oldVersions []TreeEntry
}
func GetCommitDiff(url string, commitOrRef string) ([]DiffFile, error) {
commit, err := GetSingleCommit(url, commitOrRef)
if err != nil {
return nil, err
}
added := make(map[string]TreeEntry)
deleted := make(map[string]TreeEntry)
changed := make(map[string]*changedEntry)
unchanged := make(map[string]bool)
for _, parent := range commit.Parents {
parentCommit, err := GetSingleCommit(url, parent)
if err != nil {
return nil, err
}
err = computeTreeDiffs(url, commit.Tree, parentCommit.Tree, "", added, deleted, changed, unchanged)
if err != nil {
return nil, err
}
}
var diff []DiffFile
var mu sync.Mutex
var wg sync.WaitGroup
var firstErr error
for path, entry := range changed {
p := path
e := entry
wg.Add(1)
go func() {
defer wg.Done()
curr, err := GetObject(url, e.newVersion.Hash)
if err != nil {
mu.Lock()
if firstErr == nil {
firstErr = err
}
mu.Unlock()
return
}
if curr == nil {
return
}
if len(e.oldVersions) == 0 {
return
}
oldObj, err := GetObject(url, e.oldVersions[0].Hash)
if err != nil || oldObj == nil {
return
}
if isBinary(curr.Data) || isBinary(oldObj.Data) {
mu.Lock()
diff = append(diff, DiffFile{
Path: p,
Status: "changed-binary",
})
mu.Unlock()
return
}
lines := diffTextLines(oldObj.Data, curr.Data)
mu.Lock()
diff = append(diff, DiffFile{
Path: p,
Status: "changed",
Lines: lines,
})
mu.Unlock()
}()
}
for path, entry := range deleted {
p := path
e := entry
wg.Add(1)
go func() {
defer wg.Done()
obj, err := GetObject(url, e.Hash)
if err != nil || obj == nil {
return
}
mu.Lock()
diff = append(diff, DiffFile{
Path: p,
Status: "deleted",
Content: obj.Data,
})
mu.Unlock()
}()
}
for path, entry := range added {
p := path
e := entry
wg.Add(1)
go func() {
defer wg.Done()
obj, err := GetObject(url, e.Hash)
if err != nil || obj == nil {
return
}
mu.Lock()
diff = append(diff, DiffFile{
Path: p,
Status: "added",
Content: obj.Data,
})
mu.Unlock()
}()
}
wg.Wait()
if firstErr != nil {
return nil, firstErr
}
return diff, nil
}
func isBinary(data []byte) bool {
for _, b := range data {
if b == 0 {
return true
}
}
return false
}
func diffTextLines(oldData []byte, newData []byte) []DiffLine {
oldText := string(oldData)
newText := string(newData)
oldLines := splitLines(oldText)
newLines := splitLines(newText)
ops := lcsOperations(oldLines, newLines)
allLines := make([]DiffLine, 0, len(ops))
oldIndex := 1
newIndex := 1
for i := 0; i < len(ops); i++ {
op := ops[i]
var next *lcsOp
if i+1 < len(ops) {
next = &ops[i+1]
}
if op.typ == "del" && next != nil && next.typ == "add" {
allLines = append(allLines, DiffLine{
Status: "changed",
Index: newIndex,
Text: next.line,
})
oldIndex++
newIndex++
i++
continue
}
if op.typ == "add" && next != nil && next.typ == "del" {
allLines = append(allLines, DiffLine{
Status: "changed",
Index: newIndex,
Text: op.line,
})
oldIndex++
newIndex++
i++
continue
}
if op.typ == "add" {
allLines = append(allLines, DiffLine{
Status: "added",
Index: newIndex,
Text: op.line,
})
newIndex++
continue
}
if op.typ == "del" {
allLines = append(allLines, DiffLine{
Status: "deleted",
Index: oldIndex,
Text: op.line,
})
oldIndex++
continue
}
oldIndex++
newIndex++
}
if len(allLines) == 0 {
return allLines
}
keep := make([]bool, len(allLines))
for i := 0; i < len(allLines); i++ {
start := i - 3
if start < 0 {
start = 0
}
end := i + 3
if end >= len(allLines) {
end = len(allLines) - 1
}
for j := start; j <= end; j++ {
keep[j] = true
}
}
result := make([]DiffLine, 0, len(allLines))
for i := 0; i < len(allLines); i++ {
if keep[i] {
result = append(result, allLines[i])
}
}
return result
}
type lcsOp struct {
typ string
line string
}
func splitLines(text string) []string {
lines := strings.Split(text, "\n")
if len(lines) > 0 && lines[len(lines)-1] == "" {
lines = lines[:len(lines)-1]
}
return lines
}
func lcsOperations(oldLines []string, newLines []string) []lcsOp {
n := len(oldLines)
m := len(newLines)
dp := make([][]uint32, n+1)
for i := range dp {
dp[i] = make([]uint32, m+1)
}
for i := 1; i <= n; i++ {
for j := 1; j <= m; j++ {
if oldLines[i-1] == newLines[j-1] {
dp[i][j] = dp[i-1][j-1] + 1
} else if dp[i-1][j] >= dp[i][j-1] {
dp[i][j] = dp[i-1][j]
} else {
dp[i][j] = dp[i][j-1]
}
}
}
ops := make([]lcsOp, 0, n+m)
i := n
j := m
for i > 0 || j > 0 {
if i > 0 && j > 0 && oldLines[i-1] == newLines[j-1] {
ops = append(ops, lcsOp{typ: "equal", line: oldLines[i-1]})
i--
j--
continue
}
if i > 0 && (j == 0 || dp[i-1][j] >= dp[i][j-1]) {
ops = append(ops, lcsOp{typ: "del", line: oldLines[i-1]})
i--
continue
}
if j > 0 {
ops = append(ops, lcsOp{typ: "add", line: newLines[j-1]})
j--
}
}
for i, j := 0, len(ops)-1; i < j; i, j = i+1, j-1 {
ops[i], ops[j] = ops[j], ops[i]
}
return ops
}
func computeTreeDiffs(
url string,
treeHash string,
parentTreeHash string,
basePath string,
added map[string]TreeEntry,
deleted map[string]TreeEntry,
changed map[string]*changedEntry,
unchanged map[string]bool,
) error {
var newTree []TreeEntry
var oldTree []TreeEntry
if treeHash != "" {
obj, err := GetObject(url, treeHash)
if err != nil {
return err
}
if obj != nil {
newTree = ParseTree(obj.Data)
}
}
if parentTreeHash != "" {
obj, err := GetObject(url, parentTreeHash)
if err != nil {
return err
}
if obj != nil {
oldTree = ParseTree(obj.Data)
}
}
for _, entry := range newTree {
var old *TreeEntry
for _, o := range oldTree {
if o.Path == entry.Path {
o := o
old = &o
break
}
}
if old != nil {
delete(added, basePath+entry.Path)
if old.Hash == entry.Hash {
unchanged[basePath+entry.Path] = true
} else {
if entry.IsDir {
err := computeTreeDiffs(url, entry.Hash, old.Hash, basePath+entry.Path+"/", added, deleted, changed, unchanged)
if err != nil {
return err
}
} else {
if existing, exists := changed[basePath+entry.Path]; !exists {
changed[basePath+entry.Path] = &changedEntry{
newVersion: entry,
oldVersions: []TreeEntry{*old},
}
} else {
existing.oldVersions = append(existing.oldVersions, *old)
}
}
}
} else {
if entry.IsDir {
err := computeTreeDiffs(url, entry.Hash, "", basePath+entry.Path+"/", added, deleted, changed, unchanged)
if err != nil {
return err
}
} else {
added[basePath+entry.Path] = entry
}
}
}
for _, old := range oldTree {
if unchanged[basePath+old.Path] || changed[basePath+old.Path] != nil {
continue
}
if old.IsDir {
err := computeTreeDiffs(url, "", old.Hash, basePath+old.Path+"/", added, deleted, changed, unchanged)
if err != nil {
return err
}
} else {
deleted[basePath+old.Path] = old
}
}
return nil
}