aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMáximo Cuadros <mcuadros@gmail.com>2017-05-23 15:38:54 +0200
committerGitHub <noreply@github.com>2017-05-23 15:38:54 +0200
commitf663a9384619965ed8df7a7224e6f15ad18ed4af (patch)
tree337a4615e7a3ef24f46cbe008944a687c6a964ac
parent2ff77a8d93529cefdca922dbed89d4b1cd0ee8e5 (diff)
parent65416cf6c0e8264cc7938fe0611998d52780e089 (diff)
downloadgo-git-f663a9384619965ed8df7a7224e6f15ad18ed4af.tar.gz
Merge pull request #388 from ajnavarro/feature/commit-diff
format/diff: unified diff encoder and public API
-rw-r--r--COMPATIBILITY.md2
-rw-r--r--plumbing/format/diff/patch.go58
-rw-r--r--plumbing/format/diff/unified_encoder.go355
-rw-r--r--plumbing/format/diff/unified_encoder_test.go829
-rw-r--r--plumbing/object/change.go12
-rw-r--r--plumbing/object/change_test.go25
-rw-r--r--plumbing/object/commit.go15
-rw-r--r--plumbing/object/commit_test.go54
-rw-r--r--plumbing/object/file.go12
-rw-r--r--plumbing/object/patch.go187
-rw-r--r--plumbing/object/tree.go11
-rw-r--r--utils/binary/read.go31
-rw-r--r--utils/binary/read_test.go24
13 files changed, 1614 insertions, 1 deletions
diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md
index 1c17483..cc91433 100644
--- a/COMPATIBILITY.md
+++ b/COMPATIBILITY.md
@@ -39,7 +39,7 @@ is supported by go-git.
| **patching** |
| apply | ✖ |
| cherry-pick | ✖ |
-| diff | ✖ |
+| diff | ✔ | Patch object with UnifiedDiff output representation |
| rebase | ✖ |
| revert | ✖ |
| **debugging** |
diff --git a/plumbing/format/diff/patch.go b/plumbing/format/diff/patch.go
new file mode 100644
index 0000000..7c6cf4a
--- /dev/null
+++ b/plumbing/format/diff/patch.go
@@ -0,0 +1,58 @@
+package diff
+
+import (
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/filemode"
+)
+
+// Operation defines the operation of a diff item.
+type Operation int
+
+const (
+ // Equal item represents a equals diff.
+ Equal Operation = iota
+ // Add item represents an insert diff.
+ Add
+ // Delete item represents a delete diff.
+ Delete
+)
+
+// Patch represents a collection of steps to transform several files.
+type Patch interface {
+ // FilePatches returns a slice of patches per file.
+ FilePatches() []FilePatch
+ // Message returns an optional message that can be at the top of the
+ // Patch representation.
+ Message() string
+}
+
+// FilePatch represents the necessary steps to transform one file to another.
+type FilePatch interface {
+ // IsBinary returns true if this patch is representing a binary file.
+ IsBinary() bool
+ // Files returns the from and to Files, with all the necessary metadata to
+ // about them. If the patch creates a new file, "from" will be nil.
+ // If the patch deletes a file, "to" will be nil.
+ Files() (from, to File)
+ // Chunks returns a slice of ordered changes to transform "from" File to
+ // "to" File. If the file is a binary one, Chunks will be empty.
+ Chunks() []Chunk
+}
+
+// File contains all the file metadata necessary to print some patch formats.
+type File interface {
+ // Hash returns the File Hash.
+ Hash() plumbing.Hash
+ // Mode returns the FileMode.
+ Mode() filemode.FileMode
+ // Path returns the complete Path to the file, including the filename.
+ Path() string
+}
+
+// Chunk represents a portion of a file transformation to another.
+type Chunk interface {
+ // Content contains the portion of the file.
+ Content() string
+ // Type contains the Operation to do with this Chunk.
+ Type() Operation
+}
diff --git a/plumbing/format/diff/unified_encoder.go b/plumbing/format/diff/unified_encoder.go
new file mode 100644
index 0000000..a4ff7ab
--- /dev/null
+++ b/plumbing/format/diff/unified_encoder.go
@@ -0,0 +1,355 @@
+package diff
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+
+ "gopkg.in/src-d/go-git.v4/plumbing"
+)
+
+const (
+ diffInit = "diff --git a/%s b/%s\n"
+
+ chunkStart = "@@ -"
+ chunkMiddle = " +"
+ chunkEnd = " @@%s\n"
+ chunkCount = "%d,%d"
+
+ noFilePath = "/dev/null"
+ aDir = "a/"
+ bDir = "b/"
+
+ fPath = "--- %s\n"
+ tPath = "+++ %s\n"
+ binary = "Binary files %s and %s differ\n"
+
+ addLine = "+%s\n"
+ deleteLine = "-%s\n"
+ equalLine = " %s\n"
+
+ oldMode = "old mode %o\n"
+ newMode = "new mode %o\n"
+ deletedFileMode = "deleted file mode %o\n"
+ newFileMode = "new file mode %o\n"
+
+ renameFrom = "from"
+ renameTo = "to"
+ renameFileMode = "rename %s %s\n"
+
+ indexAndMode = "index %s..%s %o\n"
+ indexNoMode = "index %s..%s\n"
+
+ DefaultContextLines = 3
+)
+
+var ErrBothFilesEmpty = errors.New("both files are empty")
+
+// UnifiedEncoder encodes an unified diff into the provided Writer.
+// There are some unsupported features:
+// - Similarity index for renames
+// - Sort hash representation
+type UnifiedEncoder struct {
+ io.Writer
+
+ // ctxLines is the count of unchanged lines that will appear
+ // surrounding a change.
+ ctxLines int
+
+ buf bytes.Buffer
+}
+
+func NewUnifiedEncoder(w io.Writer, ctxLines int) *UnifiedEncoder {
+ return &UnifiedEncoder{ctxLines: ctxLines, Writer: w}
+}
+
+func (e *UnifiedEncoder) Encode(patch Patch) error {
+ e.printMessage(patch.Message())
+
+ if err := e.encodeFilePatch(patch.FilePatches()); err != nil {
+ return err
+ }
+
+ _, err := e.buf.WriteTo(e)
+
+ return err
+}
+
+func (e *UnifiedEncoder) encodeFilePatch(filePatches []FilePatch) error {
+ for _, p := range filePatches {
+ f, t := p.Files()
+ if err := e.header(f, t, p.IsBinary()); err != nil {
+ return err
+ }
+
+ g := newHunksGenerator(p.Chunks(), e.ctxLines)
+ for _, c := range g.Generate() {
+ c.WriteTo(&e.buf)
+ }
+ }
+
+ return nil
+}
+
+func (e *UnifiedEncoder) printMessage(message string) {
+ isEmpty := message == ""
+ hasSuffix := strings.HasSuffix(message, "\n")
+ if !isEmpty && !hasSuffix {
+ message = message + "\n"
+ }
+
+ e.buf.WriteString(message)
+}
+
+func (e *UnifiedEncoder) header(from, to File, isBinary bool) error {
+ switch {
+ case from == nil && to == nil:
+ return ErrBothFilesEmpty
+ case from != nil && to != nil:
+ hashEquals := from.Hash() == to.Hash()
+
+ fmt.Fprintf(&e.buf, diffInit, from.Path(), to.Path())
+
+ if from.Mode() != to.Mode() {
+ fmt.Fprintf(&e.buf, oldMode+newMode, from.Mode(), to.Mode())
+ }
+
+ if from.Path() != to.Path() {
+ fmt.Fprintf(&e.buf,
+ renameFileMode+renameFileMode,
+ renameFrom, from.Path(), renameTo, to.Path())
+ }
+
+ if from.Mode() != to.Mode() && !hashEquals {
+ fmt.Fprintf(&e.buf, indexNoMode, from.Hash(), to.Hash())
+ } else if !hashEquals {
+ fmt.Fprintf(&e.buf, indexAndMode, from.Hash(), to.Hash(), from.Mode())
+ }
+
+ if !hashEquals {
+ e.pathLines(isBinary, aDir+from.Path(), bDir+to.Path())
+ }
+ case from == nil:
+ fmt.Fprintf(&e.buf, diffInit, to.Path(), to.Path())
+ fmt.Fprintf(&e.buf, newFileMode, to.Mode())
+ fmt.Fprintf(&e.buf, indexNoMode, plumbing.ZeroHash, to.Hash())
+ e.pathLines(isBinary, noFilePath, bDir+to.Path())
+ case to == nil:
+ fmt.Fprintf(&e.buf, diffInit, from.Path(), from.Path())
+ fmt.Fprintf(&e.buf, deletedFileMode, from.Mode())
+ fmt.Fprintf(&e.buf, indexNoMode, from.Hash(), plumbing.ZeroHash)
+ e.pathLines(isBinary, aDir+from.Path(), noFilePath)
+ }
+
+ return nil
+}
+
+func (e *UnifiedEncoder) pathLines(isBinary bool, fromPath, toPath string) {
+ format := fPath + tPath
+ if isBinary {
+ format = binary
+ }
+
+ fmt.Fprintf(&e.buf, format, fromPath, toPath)
+}
+
+type hunksGenerator struct {
+ fromLine, toLine int
+ ctxLines int
+ chunks []Chunk
+ current *hunk
+ hunks []*hunk
+ beforeContext, afterContext []string
+}
+
+func newHunksGenerator(chunks []Chunk, ctxLines int) *hunksGenerator {
+ return &hunksGenerator{
+ chunks: chunks,
+ ctxLines: ctxLines,
+ }
+}
+
+func (c *hunksGenerator) Generate() []*hunk {
+ for i, chunk := range c.chunks {
+ ls := splitLines(chunk.Content())
+ lsLen := len(ls)
+
+ switch chunk.Type() {
+ case Equal:
+ c.fromLine += lsLen
+ c.toLine += lsLen
+ c.processEqualsLines(ls, i)
+ case Delete:
+ if lsLen != 0 {
+ c.fromLine++
+ }
+
+ c.processHunk(i, chunk.Type())
+ c.fromLine += lsLen - 1
+ c.current.AddOp(chunk.Type(), ls...)
+ case Add:
+ if lsLen != 0 {
+ c.toLine++
+ }
+ c.processHunk(i, chunk.Type())
+ c.toLine += lsLen - 1
+ c.current.AddOp(chunk.Type(), ls...)
+ }
+
+ if i == len(c.chunks)-1 && c.current != nil {
+ c.hunks = append(c.hunks, c.current)
+ }
+ }
+
+ return c.hunks
+}
+
+func (c *hunksGenerator) processHunk(i int, op Operation) {
+ if c.current != nil {
+ return
+ }
+
+ var ctxPrefix string
+ linesBefore := len(c.beforeContext)
+ if linesBefore > c.ctxLines {
+ ctxPrefix = " " + c.beforeContext[linesBefore-c.ctxLines-1]
+ c.beforeContext = c.beforeContext[linesBefore-c.ctxLines:]
+ linesBefore = c.ctxLines
+ }
+
+ c.current = &hunk{ctxPrefix: ctxPrefix}
+ c.current.AddOp(Equal, c.beforeContext...)
+
+ switch op {
+ case Delete:
+ c.current.fromLine, c.current.toLine =
+ c.addLineNumbers(c.fromLine, c.toLine, linesBefore, i, Add)
+ case Add:
+ c.current.toLine, c.current.fromLine =
+ c.addLineNumbers(c.toLine, c.fromLine, linesBefore, i, Delete)
+ }
+
+ c.beforeContext = nil
+}
+
+// addLineNumbers obtains the line numbers in a new chunk
+func (c *hunksGenerator) addLineNumbers(la, lb int, linesBefore int, i int, op Operation) (cla, clb int) {
+ cla = la - linesBefore
+ // we need to search for a reference for the next diff
+ switch {
+ case linesBefore != 0 && c.ctxLines != 0:
+ clb = lb - c.ctxLines + 1
+ case c.ctxLines == 0:
+ clb = lb - c.ctxLines
+ case i != len(c.chunks)-1:
+ next := c.chunks[i+1]
+ if next.Type() == op || next.Type() == Equal {
+ // this diff will be into this chunk
+ clb = lb + 1
+ }
+ }
+
+ return
+}
+
+func (c *hunksGenerator) processEqualsLines(ls []string, i int) {
+ if c.current == nil {
+ c.beforeContext = append(c.beforeContext, ls...)
+ return
+ }
+
+ c.afterContext = append(c.afterContext, ls...)
+ if len(c.afterContext) <= c.ctxLines*2 && i != len(c.chunks)-1 {
+ c.current.AddOp(Equal, c.afterContext...)
+ c.afterContext = nil
+ } else {
+ c.current.AddOp(Equal, c.afterContext[:c.ctxLines]...)
+ c.hunks = append(c.hunks, c.current)
+
+ c.current = nil
+ c.beforeContext = c.afterContext[c.ctxLines:]
+ c.afterContext = nil
+ }
+}
+
+func splitLines(s string) []string {
+ out := strings.Split(s, "\n")
+ if out[len(out)-1] == "" {
+ out = out[:len(out)-1]
+ }
+
+ return out
+}
+
+type hunk struct {
+ fromLine int
+ toLine int
+
+ fromCount int
+ toCount int
+
+ ctxPrefix string
+ ops []*op
+}
+
+func (c *hunk) WriteTo(buf *bytes.Buffer) {
+ buf.WriteString(chunkStart)
+
+ if c.fromCount == 1 {
+ fmt.Fprintf(buf, "%d", c.fromLine)
+ } else {
+ fmt.Fprintf(buf, chunkCount, c.fromLine, c.fromCount)
+ }
+
+ buf.WriteString(chunkMiddle)
+
+ if c.toCount == 1 {
+ fmt.Fprintf(buf, "%d", c.toLine)
+ } else {
+ fmt.Fprintf(buf, chunkCount, c.toLine, c.toCount)
+ }
+
+ fmt.Fprintf(buf, chunkEnd, c.ctxPrefix)
+
+ for _, d := range c.ops {
+ buf.WriteString(d.String())
+ }
+}
+
+func (c *hunk) AddOp(t Operation, s ...string) {
+ ls := len(s)
+ switch t {
+ case Add:
+ c.toCount += ls
+ case Delete:
+ c.fromCount += ls
+ case Equal:
+ c.toCount += ls
+ c.fromCount += ls
+ }
+
+ for _, l := range s {
+ c.ops = append(c.ops, &op{l, t})
+ }
+}
+
+type op struct {
+ text string
+ t Operation
+}
+
+func (o *op) String() string {
+ var prefix string
+ switch o.t {
+ case Add:
+ prefix = addLine
+ case Delete:
+ prefix = deleteLine
+ case Equal:
+ prefix = equalLine
+ }
+
+ return fmt.Sprintf(prefix, o.text)
+}
diff --git a/plumbing/format/diff/unified_encoder_test.go b/plumbing/format/diff/unified_encoder_test.go
new file mode 100644
index 0000000..b832920
--- /dev/null
+++ b/plumbing/format/diff/unified_encoder_test.go
@@ -0,0 +1,829 @@
+package diff
+
+import (
+ "bytes"
+ "testing"
+
+ "gopkg.in/src-d/go-git.v4/plumbing"
+ "gopkg.in/src-d/go-git.v4/plumbing/filemode"
+
+ . "gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type UnifiedEncoderTestSuite struct{}
+
+var _ = Suite(&UnifiedEncoderTestSuite{})
+
+func (s *UnifiedEncoderTestSuite) TestBothFilesEmpty(c *C) {
+ buffer := bytes.NewBuffer(nil)
+ e := NewUnifiedEncoder(buffer, 1)
+ err := e.Encode(testPatch{filePatches: []testFilePatch{{}}})
+ c.Assert(err, Equals, ErrBothFilesEmpty)
+}
+
+func (s *UnifiedEncoderTestSuite) TestBinaryFile(c *C) {
+ buffer := bytes.NewBuffer(nil)
+ e := NewUnifiedEncoder(buffer, 1)
+ p := testPatch{
+ message: "",
+ filePatches: []testFilePatch{{
+ from: &testFile{
+ mode: filemode.Regular,
+ path: "binary",
+ seed: "something",
+ },
+ to: &testFile{
+ mode: filemode.Regular,
+ path: "binary",
+ seed: "otherthing",
+ },
+ }},
+ }
+
+ err := e.Encode(p)
+ c.Assert(err, IsNil)
+
+ c.Assert(buffer.String(), Equals, `diff --git a/binary b/binary
+index a459bc245bdbc45e1bca99e7fe61731da5c48da4..6879395eacf3cc7e5634064ccb617ac7aa62be7d 100644
+Binary files a/binary and b/binary differ
+`)
+}
+
+func (s *UnifiedEncoderTestSuite) TestEncode(c *C) {
+ for _, f := range fixtures {
+ c.Log("executing: ", f.desc)
+
+ buffer := bytes.NewBuffer(nil)
+ e := NewUnifiedEncoder(buffer, f.context)
+
+ err := e.Encode(f.patch)
+ c.Assert(err, IsNil)
+
+ c.Assert(buffer.String(), Equals, f.diff)
+ }
+}
+
+var oneChunkPatch Patch = testPatch{
+ message: "",
+ filePatches: []testFilePatch{{
+ from: &testFile{
+ mode: filemode.Regular,
+ path: "onechunk.txt",
+ seed: "A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM\nN\nÑ\nO\nP\nQ\nR\nS\nT\nU\nV\nW\nX\nY\nZ",
+ },
+ to: &testFile{
+ mode: filemode.Regular,
+ path: "onechunk.txt",
+ seed: "B\nC\nD\nE\nF\nG\nI\nJ\nK\nL\nM\nN\nO\nP\nQ\nR\nS\nT\nV\nW\nX\nY\nZ",
+ },
+
+ chunks: []testChunk{{
+ content: "A\n",
+ op: Delete,
+ }, {
+ content: "B\nC\nD\nE\nF\nG",
+ op: Equal,
+ }, {
+ content: "H\n",
+ op: Delete,
+ }, {
+ content: "I\nJ\nK\nL\nM\nN\n",
+ op: Equal,
+ }, {
+ content: "Ñ\n",
+ op: Delete,
+ }, {
+ content: "O\nP\nQ\nR\nS\nT\n",
+ op: Equal,
+ }, {
+ content: "U\n",
+ op: Delete,
+ }, {
+ content: "V\nW\nX\nY\nZ",
+ op: Equal,
+ }},
+ }},
+}
+
+var oneChunkPatchInverted Patch = testPatch{
+ message: "",
+ filePatches: []testFilePatch{{
+ to: &testFile{
+ mode: filemode.Regular,
+ path: "onechunk.txt",
+ seed: "A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM\nN\nÑ\nO\nP\nQ\nR\nS\nT\nU\nV\nW\nX\nY\nZ",
+ },
+ from: &testFile{
+ mode: filemode.Regular,
+ path: "onechunk.txt",
+ seed: "B\nC\nD\nE\nF\nG\nI\nJ\nK\nL\nM\nN\nO\nP\nQ\nR\nS\nT\nV\nW\nX\nY\nZ",
+ },
+
+ chunks: []testChunk{{
+ content: "A\n",
+ op: Add,
+ }, {
+ content: "B\nC\nD\nE\nF\nG",
+ op: Equal,
+ }, {
+ content: "H\n",
+ op: Add,
+ }, {
+ content: "I\nJ\nK\nL\nM\nN\n",
+ op: Equal,
+ }, {
+ content: "Ñ\n",
+ op: Add,
+ }, {
+ content: "O\nP\nQ\nR\nS\nT\n",
+ op: Equal,
+ }, {
+ content: "U\n",
+ op: Add,
+ }, {
+ content: "V\nW\nX\nY\nZ",
+ op: Equal,
+ }},
+ }},
+}
+
+var fixtures []*fixture = []*fixture{{
+ patch: testPatch{
+ message: "",
+ filePatches: []testFilePatch{{
+ from: &testFile{
+ mode: filemode.Regular,
+ path: "test.txt",
+ seed: "test",
+ },
+ to: &testFile{
+ mode: filemode.Executable,
+ path: "test.txt",
+ seed: "test",
+ },
+ chunks: nil,
+ }},
+ },
+ desc: "make executable",
+ context: 1,
+ diff: `diff --git a/test.txt b/test.txt
+old mode 100644
+new mode 100755
+`,
+}, {
+ patch: testPatch{
+ message: "",
+ filePatches: []testFilePatch{{
+ from: &testFile{
+ mode: filemode.Regular,
+ path: "test.txt",
+ seed: "test",
+ },
+ to: &testFile{
+ mode: filemode.Regular,
+ path: "test1.txt",
+ seed: "test",
+ },
+ chunks: nil,
+ }},
+ },
+ desc: "rename file",
+ context: 1,
+ diff: `diff --git a/test.txt b/test1.txt
+rename from test.txt
+rename to test1.txt
+`,
+}, {
+ patch: testPatch{
+ message: "",
+ filePatches: []testFilePatch{{
+ from: &testFile{
+ mode: filemode.Regular,
+ path: "test.txt",
+ seed: "test",
+ },
+ to: &testFile{
+ mode: filemode.Regular,
+ path: "test1.txt",
+ seed: "test1",
+ },
+ chunks: []testChunk{{
+ content: "test",
+ op: Delete,
+ }, {
+ content: "test1",
+ op: Add,
+ }},
+ }},
+ },
+ desc: "rename file with changes",
+ context: 1,
+ diff: `diff --git a/test.txt b/test1.txt
+rename from test.txt
+rename to test1.txt
+index 30d74d258442c7c65512eafab474568dd706c430..f079749c42ffdcc5f52ed2d3a6f15b09307e975e 100644
+--- a/test.txt
++++ b/test1.txt
+@@ -1 +1 @@
+-test
++test1
+`,
+}, {
+ patch: testPatch{
+ message: "",
+ filePatches: []testFilePatch{{
+ from: &testFile{
+ mode: filemode.Regular,
+ path: "test.txt",
+ seed: "test",
+ },
+ to: &testFile{
+ mode: filemode.Executable,
+ path: "test1.txt",
+ seed: "test",
+ },
+ chunks: nil,
+ }},
+ },
+ desc: "rename with file mode change",
+ context: 1,
+ diff: `diff --git a/test.txt b/test1.txt
+old mode 100644
+new mode 100755
+rename from test.txt
+rename to test1.txt
+`,
+}, {
+ patch: testPatch{
+ message: "",
+ filePatches: []testFilePatch{{
+ from: &testFile{
+ mode: filemode.Regular,
+ path: "test.txt",
+ seed: "test",
+ },
+ to: &testFile{
+ mode: filemode.Regular,
+ path: "test.txt",
+ seed: "test2",
+ },
+
+ chunks: []testChunk{{
+ content: "test",
+ op: Delete,
+ }, {
+ content: "test2",
+ op: Add,
+ }},
+ }},
+ },
+
+ desc: "one line change",
+ context: 1,
+ diff: `diff --git a/test.txt b/test.txt
+index 30d74d258442c7c65512eafab474568dd706c430..d606037cb232bfda7788a8322492312d55b2ae9d 100644
+--- a/test.txt
++++ b/test.txt
+@@ -1 +1 @@
+-test
++test2
+`,
+}, {
+ patch: testPatch{
+ message: "this is the message\n",
+ filePatches: []testFilePatch{{
+ from: &testFile{
+ mode: filemode.Regular,
+ path: "test.txt",
+ seed: "test",
+ },
+ to: &testFile{
+ mode: filemode.Regular,
+ path: "test.txt",
+ seed: "test2",
+ },
+
+ chunks: []testChunk{{
+ content: "test",
+ op: Delete,
+ }, {
+ content: "test2",
+ op: Add,
+ }},
+ }},
+ },
+
+ desc: "one line change with message",
+ context: 1,
+ diff: `this is the message
+diff --git a/test.txt b/test.txt
+index 30d74d258442c7c65512eafab474568dd706c430..d606037cb232bfda7788a8322492312d55b2ae9d 100644
+--- a/test.txt
++++ b/test.txt
+@@ -1 +1 @@
+-test
++test2
+`,
+}, {
+ patch: testPatch{
+ message: "this is the message",
+ filePatches: []testFilePatch{{
+ from: &testFile{
+ mode: filemode.Regular,
+ path: "test.txt",
+ seed: "test",
+ },
+ to: &testFile{
+ mode: filemode.Regular,
+ path: "test.txt",
+ seed: "test2",
+ },
+
+ chunks: []testChunk{{
+ content: "test",
+ op: Delete,
+ }, {
+ content: "test2",
+ op: Add,
+ }},
+ }},
+ },
+
+ desc: "one line change with message and no end of line",
+ context: 1,
+ diff: `this is the message
+diff --git a/test.txt b/test.txt
+index 30d74d258442c7c65512eafab474568dd706c430..d606037cb232bfda7788a8322492312d55b2ae9d 100644
+--- a/test.txt
++++ b/test.txt
+@@ -1 +1 @@
+-test
++test2
+`,
+}, {
+ patch: testPatch{
+ message: "",
+ filePatches: []testFilePatch{{
+ from: nil,
+ to: &testFile{
+ mode: filemode.Regular,
+ path: "new.txt",
+ seed: "test\ntest2\test3",
+ },
+
+ chunks: []testChunk{{
+ content: "test\ntest2\ntest3",
+ op: Add,
+ }},
+ }},
+ },
+
+ desc: "new file",
+ context: 1,
+ diff: `diff --git a/new.txt b/new.txt
+new file mode 100644
+index 0000000000000000000000000000000000000000..65c8dd02a42273038658a22b1cb29c8d9457ca12
+--- /dev/null
++++ b/new.txt
+@@ -0,0 +1,3 @@
++test
++test2
++test3
+`,
+}, {
+ patch: testPatch{
+ message: "",
+ filePatches: []testFilePatch{{
+ from: &testFile{
+ mode: filemode.Regular,
+ path: "old.txt",
+ seed: "test",
+ },
+ to: nil,
+
+ chunks: []testChunk{{
+ content: "test",
+ op: Delete,
+ }},
+ }},
+ },
+
+ desc: "delete file",
+ context: 1,
+ diff: `diff --git a/old.txt b/old.txt
+deleted file mode 100644
+index 30d74d258442c7c65512eafab474568dd706c430..0000000000000000000000000000000000000000
+--- a/old.txt
++++ /dev/null
+@@ -1 +0,0 @@
+-test
+`,
+}, {
+ patch: oneChunkPatch,
+ desc: "modified deleting lines file with context to 1",
+ context: 1,
+ diff: `diff --git a/onechunk.txt b/onechunk.txt
+index ab5eed5d4a2c33aeef67e0188ee79bed666bde6f..0adddcde4fd38042c354518351820eb06c417c82 100644
+--- a/onechunk.txt
++++ b/onechunk.txt
+@@ -1,2 +1 @@
+-A
+ B
+@@ -7,3 +6,2 @@ F
+ G
+-H
+ I
+@@ -14,3 +12,2 @@ M
+ N
+-Ñ
+ O
+@@ -21,3 +18,2 @@ S
+ T
+-U
+ V
+`,
+}, {
+ patch: oneChunkPatch,
+ desc: "modified deleting lines file with context to 2",
+ context: 2,
+ diff: `diff --git a/onechunk.txt b/onechunk.txt
+index ab5eed5d4a2c33aeef67e0188ee79bed666bde6f..0adddcde4fd38042c354518351820eb06c417c82 100644
+--- a/onechunk.txt
++++ b/onechunk.txt
+@@ -1,3 +1,2 @@
+-A
+ B
+ C
+@@ -6,5 +5,4 @@ E
+ F
+ G
+-H
+ I
+ J
+@@ -13,5 +11,4 @@ L
+ M
+ N
+-Ñ
+ O
+ P
+@@ -20,5 +17,4 @@ R
+ S
+ T
+-U
+ V
+ W
+`,
+}, {
+ patch: oneChunkPatch,
+
+ desc: "modified deleting lines file with context to 3",
+ context: 3,
+ diff: `diff --git a/onechunk.txt b/onechunk.txt
+index ab5eed5d4a2c33aeef67e0188ee79bed666bde6f..0adddcde4fd38042c354518351820eb06c417c82 100644
+--- a/onechunk.txt
++++ b/onechunk.txt
+@@ -1,25 +1,21 @@
+-A
+ B
+ C
+ D
+ E
+ F
+ G
+-H
+ I
+ J
+ K
+ L
+ M
+ N
+-Ñ
+ O
+ P
+ Q
+ R
+ S
+ T
+-U
+ V
+ W
+ X
+`,
+}, {
+ patch: oneChunkPatch,
+ desc: "modified deleting lines file with context to 4",
+ context: 4,
+ diff: `diff --git a/onechunk.txt b/onechunk.txt
+index ab5eed5d4a2c33aeef67e0188ee79bed666bde6f..0adddcde4fd38042c354518351820eb06c417c82 100644
+--- a/onechunk.txt
++++ b/onechunk.txt
+@@ -1,26 +1,22 @@
+-A
+ B
+ C
+ D
+ E
+ F
+ G
+-H
+ I
+ J
+ K
+ L
+ M
+ N
+-Ñ
+ O
+ P
+ Q
+ R
+ S
+ T
+-U
+ V
+ W
+ X
+ Y
+`,
+}, {
+ patch: oneChunkPatch,
+ desc: "modified deleting lines file with context to 0",
+ context: 0,
+ diff: `diff --git a/onechunk.txt b/onechunk.txt
+index ab5eed5d4a2c33aeef67e0188ee79bed666bde6f..0adddcde4fd38042c354518351820eb06c417c82 100644
+--- a/onechunk.txt
++++ b/onechunk.txt
+@@ -1 +0,0 @@
+-A
+@@ -8 +6,0 @@ G
+-H
+@@ -15 +12,0 @@ N
+-Ñ
+@@ -22 +18,0 @@ T
+-U
+`,
+}, {
+ patch: oneChunkPatchInverted,
+ desc: "modified adding lines file with context to 1",
+ context: 1,
+ diff: `diff --git a/onechunk.txt b/onechunk.txt
+index 0adddcde4fd38042c354518351820eb06c417c82..ab5eed5d4a2c33aeef67e0188ee79bed666bde6f 100644
+--- a/onechunk.txt
++++ b/onechunk.txt
+@@ -1 +1,2 @@
++A
+ B
+@@ -6,2 +7,3 @@ F
+ G
++H
+ I
+@@ -12,2 +14,3 @@ M
+ N
++Ñ
+ O
+@@ -18,2 +21,3 @@ S
+ T
++U
+ V
+`,
+}, {
+ patch: oneChunkPatchInverted,
+ desc: "modified adding lines file with context to 2",
+ context: 2,
+ diff: `diff --git a/onechunk.txt b/onechunk.txt
+index 0adddcde4fd38042c354518351820eb06c417c82..ab5eed5d4a2c33aeef67e0188ee79bed666bde6f 100644
+--- a/onechunk.txt
++++ b/onechunk.txt
+@@ -1,2 +1,3 @@
++A
+ B
+ C
+@@ -5,4 +6,5 @@ E
+ F
+ G
++H
+ I
+ J
+@@ -11,4 +13,5 @@ L
+ M
+ N
++Ñ
+ O
+ P
+@@ -17,4 +20,5 @@ R
+ S
+ T
++U
+ V
+ W
+`,
+}, {
+ patch: oneChunkPatchInverted,
+ desc: "modified adding lines file with context to 3",
+ context: 3,
+ diff: `diff --git a/onechunk.txt b/onechunk.txt
+index 0adddcde4fd38042c354518351820eb06c417c82..ab5eed5d4a2c33aeef67e0188ee79bed666bde6f 100644
+--- a/onechunk.txt
++++ b/onechunk.txt
+@@ -1,21 +1,25 @@
++A
+ B
+ C
+ D
+ E
+ F
+ G
++H
+ I
+ J
+ K
+ L
+ M
+ N
++Ñ
+ O
+ P
+ Q
+ R
+ S
+ T
++U
+ V
+ W
+ X
+`,
+}, {
+ patch: oneChunkPatchInverted,
+ desc: "modified adding lines file with context to 4",
+ context: 4,
+ diff: `diff --git a/onechunk.txt b/onechunk.txt
+index 0adddcde4fd38042c354518351820eb06c417c82..ab5eed5d4a2c33aeef67e0188ee79bed666bde6f 100644
+--- a/onechunk.txt
++++ b/onechunk.txt
+@@ -1,22 +1,26 @@
++A
+ B
+ C
+ D
+ E
+ F
+ G
++H
+ I
+ J
+ K
+ L
+ M
+ N
++Ñ
+ O
+ P
+ Q
+ R
+ S
+ T
++U
+ V
+ W
+ X
+ Y
+`,
+}, {
+ patch: oneChunkPatchInverted,
+ desc: "modified adding lines file with context to 0",
+ context: 0,
+ diff: `diff --git a/onechunk.txt b/onechunk.txt
+index 0adddcde4fd38042c354518351820eb06c417c82..ab5eed5d4a2c33aeef67e0188ee79bed666bde6f 100644
+--- a/onechunk.txt
++++ b/onechunk.txt
+@@ -0,0 +1 @@
++A
+@@ -6,0 +8 @@ G
++H
+@@ -12,0 +15 @@ N
++Ñ
+@@ -18,0 +22 @@ T
++U
+`,
+}, {
+ patch: testPatch{
+ message: "",
+ filePatches: []testFilePatch{{
+ from: &testFile{
+ mode: filemode.Regular,
+ path: "onechunk.txt",
+ seed: "B\nC\nD\nE\nF\nG\nI\nJ\nK\nL\nM\nN\nO\nP\nQ\nR\nS\nT\nV\nW\nX\nY\nZ",
+ },
+ to: &testFile{
+ mode: filemode.Regular,
+ path: "onechunk.txt",
+ seed: "B\nC\nD\nE\nF\nG\nI\nJ\nK\nL\nM\nN\nO\nP\nQ\nR\nS\nT\nV\nW\nX\nY\n",
+ },
+
+ chunks: []testChunk{{
+ content: "B\nC\nD\nE\nF\nG\nI\nJ\nK\nL\nM\nN\nO\nP\nQ\nR\nS\nT\nV\nW\nX\nY\n",
+ op: Equal,
+ }, {
+ content: "Z",
+ op: Delete,
+ }},
+ }},
+ },
+ desc: "remove last letter",
+ context: 0,
+ diff: `diff --git a/onechunk.txt b/onechunk.txt
+index 0adddcde4fd38042c354518351820eb06c417c82..553ae669c7a9303cf848fcc749a2569228ac5309 100644
+--- a/onechunk.txt
++++ b/onechunk.txt
+@@ -23 +22,0 @@ Y
+-Z
+`,
+}}
+
+type testPatch struct {
+ message string
+ filePatches []testFilePatch
+}
+
+func (t testPatch) FilePatches() []FilePatch {
+ var result []FilePatch
+ for _, f := range t.filePatches {
+ result = append(result, f)
+ }
+
+ return result
+}
+
+func (t testPatch) Message() string {
+ return t.message
+}
+
+type testFilePatch struct {
+ from, to *testFile
+ chunks []testChunk
+}
+
+func (t testFilePatch) IsBinary() bool {
+ return len(t.chunks) == 0
+}
+func (t testFilePatch) Files() (File, File) {
+ // Go is amazing
+ switch {
+ case t.from == nil && t.to == nil:
+ return nil, nil
+ case t.from == nil:
+ return nil, t.to
+ case t.to == nil:
+ return t.from, nil
+ }
+
+ return t.from, t.to
+}
+
+func (t testFilePatch) Chunks() []Chunk {
+ var result []Chunk
+ for _, c := range t.chunks {
+ result = append(result, c)
+ }
+ return result
+}
+
+type testFile struct {
+ path string
+ mode filemode.FileMode
+ seed string
+}
+
+func (t testFile) Hash() plumbing.Hash {
+ return plumbing.ComputeHash(plumbing.BlobObject, []byte(t.seed))
+}
+
+func (t testFile) Mode() filemode.FileMode {
+ return t.mode
+}
+
+func (t testFile) Path() string {
+ return t.path
+}
+
+type testChunk struct {
+ content string
+ op Operation
+}
+
+func (t testChunk) Content() string {
+ return t.content
+}
+
+func (t testChunk) Type() Operation {
+ return t.op
+}
+
+type fixture struct {
+ desc string
+ context int
+ diff string
+ patch Patch
+}
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: %s, Path: %s>", 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, "<Action: Insert, Path: examples/clone/main.go>")
}
@@ -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, "<Action: Delete, Path: utils/difftree/difftree.go>")
}
@@ -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, "<Action: Modify, Path: utils/difftree/difftree.go>")
}
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
diff --git a/utils/binary/read.go b/utils/binary/read.go
index 0bcf11a..c256ffe 100644
--- a/utils/binary/read.go
+++ b/utils/binary/read.go
@@ -3,6 +3,7 @@
package binary
import (
+ "bufio"
"encoding/binary"
"io"
@@ -122,3 +123,33 @@ func ReadHash(r io.Reader) (plumbing.Hash, error) {
return h, nil
}
+
+const sniffLen = 8000
+
+// IsBinary detects if data is a binary value based on:
+// http://git.kernel.org/cgit/git/git.git/tree/xdiff-interface.c?id=HEAD#n198
+func IsBinary(r io.Reader) (bool, error) {
+ reader := bufio.NewReader(r)
+ c := 0
+ for {
+ if c == sniffLen {
+ break
+ }
+
+ b, err := reader.ReadByte()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return false, err
+ }
+
+ if b == byte(0) {
+ return true, nil
+ }
+
+ c++
+ }
+
+ return false, nil
+}
diff --git a/utils/binary/read_test.go b/utils/binary/read_test.go
index 59dbc30..5674653 100644
--- a/utils/binary/read_test.go
+++ b/utils/binary/read_test.go
@@ -85,3 +85,27 @@ func (s *BinarySuite) TestReadHash(c *C) {
c.Assert(err, IsNil)
c.Assert(hash.String(), Equals, expected.String())
}
+
+func (s *BinarySuite) TestIsBinary(c *C) {
+ buf := bytes.NewBuffer(nil)
+ buf.Write(bytes.Repeat([]byte{'A'}, sniffLen))
+ buf.Write([]byte{0})
+ ok, err := IsBinary(buf)
+ c.Assert(err, IsNil)
+ c.Assert(ok, Equals, false)
+
+ buf.Reset()
+
+ buf.Write(bytes.Repeat([]byte{'A'}, sniffLen-1))
+ buf.Write([]byte{0})
+ ok, err = IsBinary(buf)
+ c.Assert(err, IsNil)
+ c.Assert(ok, Equals, true)
+
+ buf.Reset()
+
+ buf.Write(bytes.Repeat([]byte{'A'}, 10))
+ ok, err = IsBinary(buf)
+ c.Assert(err, IsNil)
+ c.Assert(ok, Equals, false)
+}