From 2f293f4a5214ccba5bdf0b82ff8b62ed39144078 Mon Sep 17 00:00:00 2001 From: Antonio Jesus Navarro Perez Date: Wed, 10 May 2017 16:47:15 +0200 Subject: format/diff: unified diff encoder and public API - Added Patch interface - Added a Unified Diff encoder from Patches - Added Change method to generate Patches - Added Changes method to generate Patches - Added Tree method to generate Patches - Added Commit method to generate Patches --- plumbing/object/change.go | 12 +++ plumbing/object/change_test.go | 25 ++++++ plumbing/object/commit.go | 15 ++++ plumbing/object/commit_test.go | 54 ++++++++++++ plumbing/object/file.go | 12 +++ plumbing/object/patch.go | 187 +++++++++++++++++++++++++++++++++++++++++ plumbing/object/tree.go | 11 +++ 7 files changed, 316 insertions(+) create mode 100644 plumbing/object/patch.go (limited to 'plumbing/object') diff --git a/plumbing/object/change.go b/plumbing/object/change.go index 2f702e4..729ff5a 100644 --- a/plumbing/object/change.go +++ b/plumbing/object/change.go @@ -78,6 +78,12 @@ func (c *Change) String() string { return fmt.Sprintf("", action, c.name()) } +// Patch returns a Patch with all the file changes in chunks. This +// representation can be used to create several diff outputs. +func (c *Change) Patch() (*Patch, error) { + return getPatch("", c) +} + func (c *Change) name() string { if c.From != empty { return c.From.Name @@ -126,3 +132,9 @@ func (c Changes) String() string { return buffer.String() } + +// Patch returns a Patch with all the changes in chunks. This +// representation can be used to create several diff outputs. +func (c Changes) Patch() (*Patch, error) { + return getPatch("", c...) +} diff --git a/plumbing/object/change_test.go b/plumbing/object/change_test.go index bfd4613..ded7ff2 100644 --- a/plumbing/object/change_test.go +++ b/plumbing/object/change_test.go @@ -5,6 +5,7 @@ import ( "gopkg.in/src-d/go-git.v4/plumbing" "gopkg.in/src-d/go-git.v4/plumbing/filemode" + "gopkg.in/src-d/go-git.v4/plumbing/format/diff" "gopkg.in/src-d/go-git.v4/plumbing/storer" "gopkg.in/src-d/go-git.v4/storage/filesystem" "gopkg.in/src-d/go-git.v4/utils/merkletrie" @@ -75,6 +76,12 @@ func (s *ChangeSuite) TestInsert(c *C) { c.Assert(to.Name, Equals, name) c.Assert(to.Blob.Hash, Equals, blob) + p, err := change.Patch() + c.Assert(err, IsNil) + c.Assert(len(p.FilePatches()), Equals, 1) + c.Assert(len(p.FilePatches()[0].Chunks()), Equals, 1) + c.Assert(p.FilePatches()[0].Chunks()[0].Type(), Equals, diff.Add) + str := change.String() c.Assert(str, Equals, "") } @@ -121,6 +128,12 @@ func (s *ChangeSuite) TestDelete(c *C) { c.Assert(from.Name, Equals, name) c.Assert(from.Blob.Hash, Equals, blob) + p, err := change.Patch() + c.Assert(err, IsNil) + c.Assert(len(p.FilePatches()), Equals, 1) + c.Assert(len(p.FilePatches()[0].Chunks()), Equals, 1) + c.Assert(p.FilePatches()[0].Chunks()[0].Type(), Equals, diff.Delete) + str := change.String() c.Assert(str, Equals, "") } @@ -181,6 +194,18 @@ func (s *ChangeSuite) TestModify(c *C) { c.Assert(to.Name, Equals, name) c.Assert(to.Blob.Hash, Equals, toBlob) + p, err := change.Patch() + c.Assert(err, IsNil) + c.Assert(len(p.FilePatches()), Equals, 1) + c.Assert(len(p.FilePatches()[0].Chunks()), Equals, 7) + c.Assert(p.FilePatches()[0].Chunks()[0].Type(), Equals, diff.Equal) + c.Assert(p.FilePatches()[0].Chunks()[1].Type(), Equals, diff.Delete) + c.Assert(p.FilePatches()[0].Chunks()[2].Type(), Equals, diff.Add) + c.Assert(p.FilePatches()[0].Chunks()[3].Type(), Equals, diff.Equal) + c.Assert(p.FilePatches()[0].Chunks()[4].Type(), Equals, diff.Delete) + c.Assert(p.FilePatches()[0].Chunks()[5].Type(), Equals, diff.Add) + c.Assert(p.FilePatches()[0].Chunks()[6].Type(), Equals, diff.Equal) + str := change.String() c.Assert(str, Equals, "") } diff --git a/plumbing/object/commit.go b/plumbing/object/commit.go index 0a20ae6..c5a1867 100644 --- a/plumbing/object/commit.go +++ b/plumbing/object/commit.go @@ -64,6 +64,21 @@ func (c *Commit) Tree() (*Tree, error) { return GetTree(c.s, c.TreeHash) } +// Patch returns the Patch between the actual commit and the provided one. +func (c *Commit) Patch(to *Commit) (*Patch, error) { + fromTree, err := c.Tree() + if err != nil { + return nil, err + } + + toTree, err := to.Tree() + if err != nil { + return nil, err + } + + return fromTree.Patch(toTree) +} + // Parents return a CommitIter to the parent Commits. func (c *Commit) Parents() CommitIter { return NewCommitIter(c.s, diff --git a/plumbing/object/commit_test.go b/plumbing/object/commit_test.go index 87e80a5..e89302d 100644 --- a/plumbing/object/commit_test.go +++ b/plumbing/object/commit_test.go @@ -1,6 +1,7 @@ package object import ( + "bytes" "io" "strings" "time" @@ -66,6 +67,59 @@ func (s *SuiteCommit) TestParents(c *C) { i.Close() } +func (s *SuiteCommit) TestPatch(c *C) { + from := s.commit(c, plumbing.NewHash("918c48b83bd081e863dbe1b80f8998f058cd8294")) + to := s.commit(c, plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")) + + patch, err := from.Patch(to) + c.Assert(err, IsNil) + + buf := bytes.NewBuffer(nil) + err = patch.Encode(buf) + c.Assert(err, IsNil) + + c.Assert(buf.String(), Equals, `diff --git a/vendor/foo.go b/vendor/foo.go +new file mode 100644 +index 0000000000000000000000000000000000000000..9dea2395f5403188298c1dabe8bdafe562c491e3 +--- /dev/null ++++ b/vendor/foo.go +@@ -0,0 +1,7 @@ ++package main ++ ++import "fmt" ++ ++func main() { ++ fmt.Println("Hello, playground") ++} +`) + c.Assert(buf.String(), Equals, patch.String()) + + from = s.commit(c, plumbing.NewHash("b8e471f58bcbca63b07bda20e428190409c2db47")) + to = s.commit(c, plumbing.NewHash("35e85108805c84807bc66a02d91535e1e24b38b9")) + + patch, err = from.Patch(to) + c.Assert(err, IsNil) + + buf.Reset() + err = patch.Encode(buf) + c.Assert(err, IsNil) + + c.Assert(buf.String(), Equals, `diff --git a/CHANGELOG b/CHANGELOG +deleted file mode 100644 +index d3ff53e0564a9f87d8e84b6e28e5060e517008aa..0000000000000000000000000000000000000000 +--- a/CHANGELOG ++++ /dev/null +@@ -1 +0,0 @@ +-Initial changelog +diff --git a/binary.jpg b/binary.jpg +new file mode 100644 +index 0000000000000000000000000000000000000000..d5c0f4ab811897cadf03aec358ae60d21f91c50d +Binary files /dev/null and b/binary.jpg differ +`) + + c.Assert(buf.String(), Equals, patch.String()) +} + func (s *SuiteCommit) TestCommitEncodeDecodeIdempotent(c *C) { ts, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05-07:00") c.Assert(err, IsNil) diff --git a/plumbing/object/file.go b/plumbing/object/file.go index 6932c31..79f57fe 100644 --- a/plumbing/object/file.go +++ b/plumbing/object/file.go @@ -7,6 +7,7 @@ import ( "gopkg.in/src-d/go-git.v4/plumbing/filemode" "gopkg.in/src-d/go-git.v4/plumbing/storer" + "gopkg.in/src-d/go-git.v4/utils/binary" "gopkg.in/src-d/go-git.v4/utils/ioutil" ) @@ -42,6 +43,17 @@ func (f *File) Contents() (content string, err error) { return buf.String(), nil } +// IsBinary returns if the file is binary or not +func (f *File) IsBinary() (bool, error) { + reader, err := f.Reader() + if err != nil { + return false, err + } + defer ioutil.CheckClose(reader, &err) + + return binary.IsBinary(reader) +} + // Lines returns a slice of lines from the contents of a file, stripping // all end of line characters. If the last line is empty (does not end // in an end of line), it is also stripped. diff --git a/plumbing/object/patch.go b/plumbing/object/patch.go new file mode 100644 index 0000000..d413114 --- /dev/null +++ b/plumbing/object/patch.go @@ -0,0 +1,187 @@ +package object + +import ( + "bytes" + "fmt" + "io" + + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/filemode" + fdiff "gopkg.in/src-d/go-git.v4/plumbing/format/diff" + "gopkg.in/src-d/go-git.v4/utils/diff" + + dmp "github.com/sergi/go-diff/diffmatchpatch" +) + +func getPatch(message string, changes ...*Change) (*Patch, error) { + var filePatches []fdiff.FilePatch + for _, c := range changes { + fp, err := filePatch(c) + if err != nil { + return nil, err + } + + filePatches = append(filePatches, fp) + } + + return &Patch{message, filePatches}, nil +} + +func filePatch(c *Change) (fdiff.FilePatch, error) { + from, to, err := c.Files() + if err != nil { + return nil, err + } + fromContent, fIsBinary, err := fileContent(from) + if err != nil { + return nil, err + } + + toContent, tIsBinary, err := fileContent(to) + if err != nil { + return nil, err + } + + if fIsBinary || tIsBinary { + return &textFilePatch{from: c.From, to: c.To}, nil + } + + diffs := diff.Do(fromContent, toContent) + + var chunks []fdiff.Chunk + for _, d := range diffs { + var op fdiff.Operation + switch d.Type { + case dmp.DiffEqual: + op = fdiff.Equal + case dmp.DiffDelete: + op = fdiff.Delete + case dmp.DiffInsert: + op = fdiff.Add + } + + chunks = append(chunks, &textChunk{d.Text, op}) + } + + return &textFilePatch{ + chunks: chunks, + from: c.From, + to: c.To, + }, nil +} + +func fileContent(f *File) (content string, isBinary bool, err error) { + if f == nil { + return + } + + isBinary, err = f.IsBinary() + if err != nil || isBinary { + return + } + + content, err = f.Contents() + + return +} + +// textPatch is an implementation of fdiff.Patch interface +type Patch struct { + message string + filePatches []fdiff.FilePatch +} + +func (t *Patch) FilePatches() []fdiff.FilePatch { + return t.filePatches +} + +func (t *Patch) Message() string { + return t.message +} + +func (p *Patch) Encode(w io.Writer) error { + ue := fdiff.NewUnifiedEncoder(w, fdiff.DefaultContextLines) + + return ue.Encode(p) +} + +func (p *Patch) String() string { + buf := bytes.NewBuffer(nil) + err := p.Encode(buf) + if err != nil { + return fmt.Sprintf("malformed patch: %s", err.Error()) + } + + return buf.String() +} + +// changeEntryWrapper is an implementation of fdiff.File interface +type changeEntryWrapper struct { + ce ChangeEntry +} + +func (f *changeEntryWrapper) Hash() plumbing.Hash { + if !f.ce.TreeEntry.Mode.IsFile() { + return plumbing.ZeroHash + } + + return f.ce.TreeEntry.Hash +} + +func (f *changeEntryWrapper) Mode() filemode.FileMode { + return f.ce.TreeEntry.Mode +} +func (f *changeEntryWrapper) Path() string { + if !f.ce.TreeEntry.Mode.IsFile() { + return "" + } + + return f.ce.Name +} + +func (f *changeEntryWrapper) Empty() bool { + return !f.ce.TreeEntry.Mode.IsFile() +} + +// textFilePatch is an implementation of fdiff.FilePatch interface +type textFilePatch struct { + chunks []fdiff.Chunk + from, to ChangeEntry +} + +func (tf *textFilePatch) Files() (from fdiff.File, to fdiff.File) { + f := &changeEntryWrapper{tf.from} + t := &changeEntryWrapper{tf.to} + + if !f.Empty() { + from = f + } + + if !t.Empty() { + to = t + } + + return +} + +func (t *textFilePatch) IsBinary() bool { + return len(t.chunks) == 0 +} + +func (t *textFilePatch) Chunks() []fdiff.Chunk { + return t.chunks +} + +// textChunk is an implementation of fdiff.Chunk interface +type textChunk struct { + content string + op fdiff.Operation +} + +func (t *textChunk) Content() string { + return t.content +} + +func (t *textChunk) Type() fdiff.Operation { + return t.op +} diff --git a/plumbing/object/tree.go b/plumbing/object/tree.go index 25687b0..512db9f 100644 --- a/plumbing/object/tree.go +++ b/plumbing/object/tree.go @@ -270,6 +270,17 @@ func (from *Tree) Diff(to *Tree) (Changes, error) { return DiffTree(from, to) } +// Patch returns a slice of Patch objects with all the changes between trees +// in chunks. This representation can be used to create several diff outputs. +func (from *Tree) Patch(to *Tree) (*Patch, error) { + changes, err := DiffTree(from, to) + if err != nil { + return nil, err + } + + return changes.Patch() +} + // treeEntryIter facilitates iterating through the TreeEntry objects in a Tree. type treeEntryIter struct { t *Tree -- cgit