mirror of https://github.com/mickael-menu/zk
Add note files diffing
parent
6831823e5d
commit
75d1a143e7
@ -0,0 +1,110 @@
|
||||
package note
|
||||
|
||||
// DiffChange represents a note change made in a slip box directory.
|
||||
type DiffChange struct {
|
||||
Path Path
|
||||
Kind DiffKind
|
||||
}
|
||||
|
||||
// DiffKind represents a type of note change made in a slip box directory.
|
||||
type DiffKind int
|
||||
|
||||
const (
|
||||
DiffAdded DiffKind = iota + 1
|
||||
DiffModified
|
||||
DiffRemoved
|
||||
)
|
||||
|
||||
// Diff compares two sources of FileMetadata and report the note changes, using
|
||||
// the file checksum and modification date.
|
||||
//
|
||||
// Warning: The FileMetadata have to be sorted by their Path for the diffing to
|
||||
// work properly.
|
||||
func Diff(source, target <-chan FileMetadata) <-chan DiffChange {
|
||||
c := make(chan DiffChange)
|
||||
go func() {
|
||||
defer close(c)
|
||||
|
||||
pair := diffPair{}
|
||||
var sourceFile, targetFile FileMetadata
|
||||
var sourceOpened, targetOpened bool = true, true
|
||||
|
||||
for sourceOpened || targetOpened {
|
||||
if pair.source == nil {
|
||||
sourceFile, sourceOpened = <-source
|
||||
if sourceOpened {
|
||||
pair.source = &sourceFile
|
||||
}
|
||||
}
|
||||
if pair.target == nil {
|
||||
targetFile, targetOpened = <-target
|
||||
if targetOpened {
|
||||
pair.target = &targetFile
|
||||
}
|
||||
}
|
||||
change := pair.diff()
|
||||
if change != nil {
|
||||
c <- *change
|
||||
}
|
||||
}
|
||||
}()
|
||||
return c
|
||||
}
|
||||
|
||||
// diffPair holds the current two files to be diffed.
|
||||
type diffPair struct {
|
||||
source *FileMetadata
|
||||
target *FileMetadata
|
||||
}
|
||||
|
||||
// diff compares the source and target files in the current pair.
|
||||
//
|
||||
// If the source and target file are at the same path, we check for any change.
|
||||
// If the files are different, that means that either the source file was
|
||||
// added, or the target file was removed.
|
||||
func (p *diffPair) diff() *DiffChange {
|
||||
var change *DiffChange
|
||||
|
||||
switch {
|
||||
case p.source == nil && p.target == nil: // Both channels are closed
|
||||
break
|
||||
|
||||
case p.source == nil && p.target != nil: // Source channel is closed
|
||||
change = &DiffChange{p.target.Path, DiffRemoved}
|
||||
p.target = nil
|
||||
|
||||
case p.source != nil && p.target == nil: // Target channel is closed
|
||||
change = &DiffChange{p.source.Path, DiffAdded}
|
||||
p.source = nil
|
||||
|
||||
case p.source.Path == p.target.Path: // Same files, compare their modification date.
|
||||
if p.source.Modified != p.target.Modified {
|
||||
change = &DiffChange{p.source.Path, DiffModified}
|
||||
}
|
||||
p.source = nil
|
||||
p.target = nil
|
||||
|
||||
default: // Different files, one has been added or removed.
|
||||
if isAscendingOrder(p.source.Path, p.target.Path) {
|
||||
change = &DiffChange{p.source.Path, DiffAdded}
|
||||
p.source = nil
|
||||
} else {
|
||||
change = &DiffChange{p.target.Path, DiffRemoved}
|
||||
p.target = nil
|
||||
}
|
||||
}
|
||||
|
||||
return change
|
||||
}
|
||||
|
||||
// isAscendingOrder returns true if the source note's path is before the target one.
|
||||
func isAscendingOrder(source, target Path) bool {
|
||||
switch {
|
||||
case source.Dir < target.Dir:
|
||||
return true
|
||||
case source.Dir > target.Dir:
|
||||
return false
|
||||
default:
|
||||
return source.Filename < target.Filename
|
||||
}
|
||||
}
|
@ -0,0 +1,219 @@
|
||||
package note
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mickael-menu/zk/util/assert"
|
||||
)
|
||||
|
||||
var date1 = time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
|
||||
var date2 = time.Date(2012, 10, 20, 12, 34, 58, 651387237, time.UTC)
|
||||
var date3 = time.Date(2014, 12, 10, 3, 34, 58, 651387237, time.UTC)
|
||||
var date4 = time.Date(2016, 13, 11, 4, 34, 58, 651387237, time.UTC)
|
||||
|
||||
func TestDiffEmpty(t *testing.T) {
|
||||
source := []FileMetadata{}
|
||||
target := []FileMetadata{}
|
||||
test(t, source, target, []DiffChange{})
|
||||
}
|
||||
|
||||
func TestNoDiff(t *testing.T) {
|
||||
files := []FileMetadata{
|
||||
{
|
||||
Path: Path{Dir: "a", Filename: "1"},
|
||||
Modified: date1,
|
||||
},
|
||||
{
|
||||
Path: Path{Dir: "a", Filename: "2"},
|
||||
Modified: date2,
|
||||
},
|
||||
{
|
||||
Path: Path{Dir: "b", Filename: "1"},
|
||||
Modified: date3,
|
||||
},
|
||||
}
|
||||
|
||||
test(t, files, files, []DiffChange{})
|
||||
}
|
||||
|
||||
func TestDiff(t *testing.T) {
|
||||
source := []FileMetadata{
|
||||
{
|
||||
Path: Path{Dir: "a", Filename: "1"},
|
||||
Modified: date1,
|
||||
},
|
||||
{
|
||||
Path: Path{Dir: "a", Filename: "2"},
|
||||
Modified: date2,
|
||||
},
|
||||
{
|
||||
Path: Path{Dir: "b", Filename: "1"},
|
||||
Modified: date3,
|
||||
},
|
||||
}
|
||||
|
||||
target := []FileMetadata{
|
||||
{
|
||||
// Date changed
|
||||
Path: Path{Dir: "a", Filename: "1"},
|
||||
Modified: date1.Add(time.Hour),
|
||||
},
|
||||
// 2 is added
|
||||
{
|
||||
// 3 is removed
|
||||
Path: Path{Dir: "a", Filename: "3"},
|
||||
Modified: date3,
|
||||
},
|
||||
{
|
||||
// No change
|
||||
Path: Path{Dir: "b", Filename: "1"},
|
||||
Modified: date3,
|
||||
},
|
||||
}
|
||||
|
||||
test(t, source, target, []DiffChange{
|
||||
{
|
||||
Path: Path{Dir: "a", Filename: "1"},
|
||||
Kind: DiffModified,
|
||||
},
|
||||
{
|
||||
Path: Path{Dir: "a", Filename: "2"},
|
||||
Kind: DiffAdded,
|
||||
},
|
||||
{
|
||||
Path: Path{Dir: "a", Filename: "3"},
|
||||
Kind: DiffRemoved,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestDiffWithMoreInSource(t *testing.T) {
|
||||
source := []FileMetadata{
|
||||
{
|
||||
Path: Path{Dir: "a", Filename: "1"},
|
||||
Modified: date1,
|
||||
},
|
||||
{
|
||||
Path: Path{Dir: "a", Filename: "2"},
|
||||
Modified: date2,
|
||||
},
|
||||
}
|
||||
|
||||
target := []FileMetadata{
|
||||
{
|
||||
Path: Path{Dir: "a", Filename: "1"},
|
||||
Modified: date1,
|
||||
},
|
||||
}
|
||||
|
||||
test(t, source, target, []DiffChange{
|
||||
{
|
||||
Path: Path{Dir: "a", Filename: "2"},
|
||||
Kind: DiffAdded,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestDiffWithMoreInTarget(t *testing.T) {
|
||||
source := []FileMetadata{
|
||||
{
|
||||
Path: Path{Dir: "a", Filename: "1"},
|
||||
Modified: date1,
|
||||
},
|
||||
}
|
||||
|
||||
target := []FileMetadata{
|
||||
{
|
||||
Path: Path{Dir: "a", Filename: "1"},
|
||||
Modified: date1,
|
||||
},
|
||||
{
|
||||
Path: Path{Dir: "a", Filename: "2"},
|
||||
Modified: date2,
|
||||
},
|
||||
}
|
||||
|
||||
test(t, source, target, []DiffChange{
|
||||
{
|
||||
Path: Path{Dir: "a", Filename: "2"},
|
||||
Kind: DiffRemoved,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestDiffEmptySource(t *testing.T) {
|
||||
source := []FileMetadata{}
|
||||
|
||||
target := []FileMetadata{
|
||||
{
|
||||
Path: Path{Dir: "a", Filename: "1"},
|
||||
Modified: date1,
|
||||
},
|
||||
{
|
||||
Path: Path{Dir: "a", Filename: "2"},
|
||||
Modified: date2,
|
||||
},
|
||||
}
|
||||
|
||||
test(t, source, target, []DiffChange{
|
||||
{
|
||||
Path: Path{Dir: "a", Filename: "1"},
|
||||
Kind: DiffRemoved,
|
||||
},
|
||||
{
|
||||
Path: Path{Dir: "a", Filename: "2"},
|
||||
Kind: DiffRemoved,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestDiffEmptyTarget(t *testing.T) {
|
||||
source := []FileMetadata{
|
||||
{
|
||||
Path: Path{Dir: "a", Filename: "1"},
|
||||
Modified: date1,
|
||||
},
|
||||
{
|
||||
Path: Path{Dir: "a", Filename: "2"},
|
||||
Modified: date2,
|
||||
},
|
||||
}
|
||||
|
||||
target := []FileMetadata{}
|
||||
|
||||
test(t, source, target, []DiffChange{
|
||||
{
|
||||
Path: Path{Dir: "a", Filename: "1"},
|
||||
Kind: DiffAdded,
|
||||
},
|
||||
{
|
||||
Path: Path{Dir: "a", Filename: "2"},
|
||||
Kind: DiffAdded,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func test(t *testing.T, source, target []FileMetadata, expected []DiffChange) {
|
||||
actual := toSlice(Diff(toChannel(source), toChannel(target)))
|
||||
assert.Equal(t, actual, expected)
|
||||
}
|
||||
|
||||
func toChannel(fm []FileMetadata) <-chan FileMetadata {
|
||||
c := make(chan FileMetadata)
|
||||
go func() {
|
||||
for _, m := range fm {
|
||||
c <- m
|
||||
}
|
||||
close(c)
|
||||
}()
|
||||
return c
|
||||
}
|
||||
|
||||
func toSlice(c <-chan DiffChange) []DiffChange {
|
||||
s := make([]DiffChange, 0)
|
||||
for i := range c {
|
||||
s = append(s, i)
|
||||
}
|
||||
return s
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package note
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Path holds a note path relative to its slip box.
|
||||
type Path struct {
|
||||
Dir string
|
||||
Filename string
|
||||
}
|
||||
|
||||
// FileMetadata holds information about a note file.
|
||||
type FileMetadata struct {
|
||||
Path Path
|
||||
Created time.Time
|
||||
Modified time.Time
|
||||
}
|
||||
|
||||
// Metadata holds information about a particular note.
|
||||
type Metadata struct {
|
||||
Title string
|
||||
Content string
|
||||
WordCount int
|
||||
}
|
Loading…
Reference in New Issue