Add note files diffing

pull/6/head
Mickaël Menu 4 years ago
parent 6831823e5d
commit 75d1a143e7
No known key found for this signature in database
GPG Key ID: 53D73664CD359895

@ -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
}

@ -139,6 +139,9 @@ func (zk *Zk) DirAt(path string, overrides ...ConfigOverrides) (*Dir, error) {
if err != nil {
return nil, wrap(err)
}
if name == "." {
name = ""
}
config, ok := zk.Config.Dirs[name]
if !ok {

@ -39,8 +39,10 @@ func TestDirAtGivenPath(t *testing.T) {
// When requesting the root directory `.`, the config is the default one.
func TestDirAtRoot(t *testing.T) {
wd, _ := os.Getwd()
zk := Zk{
Path: "/test",
Path: wd,
Config: Config{
DirConfig: DirConfig{
FilenameTemplate: "{{id}}.note",
@ -64,6 +66,8 @@ func TestDirAtRoot(t *testing.T) {
dir, err := zk.DirAt(".")
assert.Nil(t, err)
assert.Equal(t, dir.Name, "")
assert.Equal(t, dir.Path, wd)
assert.Equal(t, dir.Config, DirConfig{
FilenameTemplate: "{{id}}.note",
BodyTemplatePath: opt.NewString("default.note"),

Loading…
Cancel
Save