aboutsummaryrefslogtreecommitdiffstats
path: root/plumbing/object
diff options
context:
space:
mode:
Diffstat (limited to 'plumbing/object')
-rw-r--r--plumbing/object/commit.go6
-rw-r--r--plumbing/object/commit_test.go8
-rw-r--r--plumbing/object/commit_walker_path.go19
-rw-r--r--plumbing/object/commit_walker_test.go26
-rw-r--r--plumbing/object/patch.go99
-rw-r--r--plumbing/object/patch_stats_test.go54
-rw-r--r--plumbing/object/patch_test.go110
-rw-r--r--plumbing/object/tree.go32
-rw-r--r--plumbing/object/tree_test.go26
-rw-r--r--plumbing/object/treenoder.go4
10 files changed, 314 insertions, 70 deletions
diff --git a/plumbing/object/commit.go b/plumbing/object/commit.go
index ceed5d0..3d096e1 100644
--- a/plumbing/object/commit.go
+++ b/plumbing/object/commit.go
@@ -27,7 +27,7 @@ const (
// the commit with the "mergetag" header.
headermergetag string = "mergetag"
- defaultUtf8CommitMesageEncoding MessageEncoding = "UTF-8"
+ defaultUtf8CommitMessageEncoding MessageEncoding = "UTF-8"
)
// Hash represents the hash of an object
@@ -189,7 +189,7 @@ func (c *Commit) Decode(o plumbing.EncodedObject) (err error) {
}
c.Hash = o.Hash()
- c.Encoding = defaultUtf8CommitMesageEncoding
+ c.Encoding = defaultUtf8CommitMessageEncoding
reader, err := o.Reader()
if err != nil {
@@ -335,7 +335,7 @@ func (c *Commit) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
}
}
- if string(c.Encoding) != "" && c.Encoding != defaultUtf8CommitMesageEncoding {
+ if string(c.Encoding) != "" && c.Encoding != defaultUtf8CommitMessageEncoding {
if _, err = fmt.Fprintf(w, "\n%s %s", headerencoding, c.Encoding); err != nil {
return err
}
diff --git a/plumbing/object/commit_test.go b/plumbing/object/commit_test.go
index 3e1fe1b..a048926 100644
--- a/plumbing/object/commit_test.go
+++ b/plumbing/object/commit_test.go
@@ -228,7 +228,7 @@ change
Message: "Message\n\nFoo\nBar\nWith trailing blank lines\n\n",
TreeHash: plumbing.NewHash("f000000000000000000000000000000000000001"),
ParentHashes: []plumbing.Hash{plumbing.NewHash("f000000000000000000000000000000000000002")},
- Encoding: defaultUtf8CommitMesageEncoding,
+ Encoding: defaultUtf8CommitMessageEncoding,
},
{
Author: Signature{Name: "Foo", Email: "foo@example.local", When: ts},
@@ -253,7 +253,7 @@ change
plumbing.NewHash("f000000000000000000000000000000000000003"),
},
MergeTag: tag,
- Encoding: defaultUtf8CommitMesageEncoding,
+ Encoding: defaultUtf8CommitMessageEncoding,
},
{
Author: Signature{Name: "Foo", Email: "foo@example.local", When: ts},
@@ -266,7 +266,7 @@ change
},
MergeTag: tag,
PGPSignature: pgpsignature,
- Encoding: defaultUtf8CommitMesageEncoding,
+ Encoding: defaultUtf8CommitMessageEncoding,
},
}
for _, commit := range commits {
@@ -455,7 +455,7 @@ func (s *SuiteCommit) TestStat(c *C) {
c.Assert(fileStats[1].Name, Equals, "php/crappy.php")
c.Assert(fileStats[1].Addition, Equals, 259)
c.Assert(fileStats[1].Deletion, Equals, 0)
- c.Assert(fileStats[1].String(), Equals, " php/crappy.php | 259 ++++++++++++++++++++++++++++++++++++++++++++++++++++\n")
+ c.Assert(fileStats[1].String(), Equals, " php/crappy.php | 259 +++++++++++++++++++++++++++++++++++++++++++++++++++++\n")
}
func (s *SuiteCommit) TestVerify(c *C) {
diff --git a/plumbing/object/commit_walker_path.go b/plumbing/object/commit_walker_path.go
index aa0ca15..c1ec8ba 100644
--- a/plumbing/object/commit_walker_path.go
+++ b/plumbing/object/commit_walker_path.go
@@ -57,6 +57,8 @@ func (c *commitPathIter) Next() (*Commit, error) {
}
func (c *commitPathIter) getNextFileCommit() (*Commit, error) {
+ var parentTree, currentTree *Tree
+
for {
// Parent-commit can be nil if the current-commit is the initial commit
parentCommit, parentCommitErr := c.sourceIter.Next()
@@ -68,13 +70,17 @@ func (c *commitPathIter) getNextFileCommit() (*Commit, error) {
parentCommit = nil
}
- // Fetch the trees of the current and parent commits
- currentTree, currTreeErr := c.currentCommit.Tree()
- if currTreeErr != nil {
- return nil, currTreeErr
+ if parentTree == nil {
+ var currTreeErr error
+ currentTree, currTreeErr = c.currentCommit.Tree()
+ if currTreeErr != nil {
+ return nil, currTreeErr
+ }
+ } else {
+ currentTree = parentTree
+ parentTree = nil
}
- var parentTree *Tree
if parentCommit != nil {
var parentTreeErr error
parentTree, parentTreeErr = parentCommit.Tree()
@@ -115,7 +121,8 @@ func (c *commitPathIter) hasFileChange(changes Changes, parent *Commit) bool {
// filename matches, now check if source iterator contains all commits (from all refs)
if c.checkParent {
- if parent != nil && isParentHash(parent.Hash, c.currentCommit) {
+ // Check if parent is beyond the initial commit
+ if parent == nil || isParentHash(parent.Hash, c.currentCommit) {
return true
}
continue
diff --git a/plumbing/object/commit_walker_test.go b/plumbing/object/commit_walker_test.go
index c47d68b..fa0ca7d 100644
--- a/plumbing/object/commit_walker_test.go
+++ b/plumbing/object/commit_walker_test.go
@@ -228,3 +228,29 @@ func (s *CommitWalkerSuite) TestCommitBSFIteratorWithIgnore(c *C) {
c.Assert(commit.Hash.String(), Equals, expected[i])
}
}
+
+func (s *CommitWalkerSuite) TestCommitPathIteratorInitialCommit(c *C) {
+ commit := s.commit(c, plumbing.NewHash(s.Fixture.Head))
+
+ fileName := "LICENSE"
+
+ var commits []*Commit
+ NewCommitPathIterFromIter(
+ func(path string) bool { return path == fileName },
+ NewCommitIterCTime(commit, nil, nil),
+ true,
+ ).ForEach(func(c *Commit) error {
+ commits = append(commits, c)
+ return nil
+ })
+
+ expected := []string{
+ "b029517f6300c2da0f4b651b8642506cd6aaf45d",
+ }
+
+ c.Assert(commits, HasLen, len(expected))
+
+ for i, commit := range commits {
+ c.Assert(commit.Hash.String(), Equals, expected[i])
+ }
+}
diff --git a/plumbing/object/patch.go b/plumbing/object/patch.go
index 06bc35b..3c61f62 100644
--- a/plumbing/object/patch.go
+++ b/plumbing/object/patch.go
@@ -6,7 +6,7 @@ import (
"errors"
"fmt"
"io"
- "math"
+ "strconv"
"strings"
"github.com/go-git/go-git/v5/plumbing"
@@ -234,69 +234,56 @@ func (fileStats FileStats) String() string {
return printStat(fileStats)
}
+// printStat prints the stats of changes in content of files.
+// Original implementation: https://github.com/git/git/blob/1a87c842ece327d03d08096395969aca5e0a6996/diff.c#L2615
+// Parts of the output:
+// <pad><filename><pad>|<pad><changeNumber><pad><+++/---><newline>
+// example: " main.go | 10 +++++++--- "
func printStat(fileStats []FileStat) string {
- padLength := float64(len(" "))
- newlineLength := float64(len("\n"))
- separatorLength := float64(len("|"))
- // Soft line length limit. The text length calculation below excludes
- // length of the change number. Adding that would take it closer to 80,
- // but probably not more than 80, until it's a huge number.
- lineLength := 72.0
-
- // Get the longest filename and longest total change.
- var longestLength float64
- var longestTotalChange float64
- for _, fs := range fileStats {
- if int(longestLength) < len(fs.Name) {
- longestLength = float64(len(fs.Name))
- }
- totalChange := fs.Addition + fs.Deletion
- if int(longestTotalChange) < totalChange {
- longestTotalChange = float64(totalChange)
- }
- }
-
- // Parts of the output:
- // <pad><filename><pad>|<pad><changeNumber><pad><+++/---><newline>
- // example: " main.go | 10 +++++++--- "
-
- // <pad><filename><pad>
- leftTextLength := padLength + longestLength + padLength
-
- // <pad><number><pad><+++++/-----><newline>
- // Excluding number length here.
- rightTextLength := padLength + padLength + newlineLength
+ maxGraphWidth := uint(53)
+ maxNameLen := 0
+ maxChangeLen := 0
- totalTextArea := leftTextLength + separatorLength + rightTextLength
- heightOfHistogram := lineLength - totalTextArea
+ scaleLinear := func(it, width, max uint) uint {
+ if it == 0 || max == 0 {
+ return 0
+ }
- // Scale the histogram.
- var scaleFactor float64
- if longestTotalChange > heightOfHistogram {
- // Scale down to heightOfHistogram.
- scaleFactor = longestTotalChange / heightOfHistogram
- } else {
- scaleFactor = 1.0
+ return 1 + (it * (width - 1) / max)
}
- finalOutput := ""
for _, fs := range fileStats {
- addn := float64(fs.Addition)
- deln := float64(fs.Deletion)
- addc := int(math.Floor(addn/scaleFactor))
- delc := int(math.Floor(deln/scaleFactor))
- if addc < 0 {
- addc = 0
+ if len(fs.Name) > maxNameLen {
+ maxNameLen = len(fs.Name)
}
- if delc < 0 {
- delc = 0
+
+ changes := strconv.Itoa(fs.Addition + fs.Deletion)
+ if len(changes) > maxChangeLen {
+ maxChangeLen = len(changes)
}
- adds := strings.Repeat("+", addc)
- dels := strings.Repeat("-", delc)
- finalOutput += fmt.Sprintf(" %s | %d %s%s\n", fs.Name, (fs.Addition + fs.Deletion), adds, dels)
}
- return finalOutput
+ result := ""
+ for _, fs := range fileStats {
+ add := uint(fs.Addition)
+ del := uint(fs.Deletion)
+ np := maxNameLen - len(fs.Name)
+ cp := maxChangeLen - len(strconv.Itoa(fs.Addition+fs.Deletion))
+
+ total := add + del
+ if total > maxGraphWidth {
+ add = scaleLinear(add, maxGraphWidth, total)
+ del = scaleLinear(del, maxGraphWidth, total)
+ }
+
+ adds := strings.Repeat("+", int(add))
+ dels := strings.Repeat("-", int(del))
+ namePad := strings.Repeat(" ", np)
+ changePad := strings.Repeat(" ", cp)
+
+ result += fmt.Sprintf(" %s%s | %s%d %s%s\n", fs.Name, namePad, changePad, total, adds, dels)
+ }
+ return result
}
func getFileStatsFromFilePatches(filePatches []fdiff.FilePatch) FileStats {
@@ -317,8 +304,8 @@ func getFileStatsFromFilePatches(filePatches []fdiff.FilePatch) FileStats {
// File is deleted.
cs.Name = from.Path()
} else if from.Path() != to.Path() {
- // File is renamed. Not supported.
- // cs.Name = fmt.Sprintf("%s => %s", from.Path(), to.Path())
+ // File is renamed.
+ cs.Name = fmt.Sprintf("%s => %s", from.Path(), to.Path())
} else {
cs.Name = from.Path()
}
diff --git a/plumbing/object/patch_stats_test.go b/plumbing/object/patch_stats_test.go
new file mode 100644
index 0000000..f393c30
--- /dev/null
+++ b/plumbing/object/patch_stats_test.go
@@ -0,0 +1,54 @@
+package object_test
+
+import (
+ "time"
+
+ "github.com/go-git/go-billy/v5/memfs"
+ "github.com/go-git/go-billy/v5/util"
+ "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/plumbing/object"
+ "github.com/go-git/go-git/v5/storage/memory"
+
+ fixtures "github.com/go-git/go-git-fixtures/v4"
+ . "gopkg.in/check.v1"
+)
+
+type PatchStatsSuite struct {
+ fixtures.Suite
+}
+
+var _ = Suite(&PatchStatsSuite{})
+
+func (s *PatchStatsSuite) TestStatsWithRename(c *C) {
+ cm := &git.CommitOptions{
+ Author: &object.Signature{Name: "Foo", Email: "foo@example.local", When: time.Now()},
+ }
+
+ fs := memfs.New()
+ r, err := git.Init(memory.NewStorage(), fs)
+ c.Assert(err, IsNil)
+
+ w, err := r.Worktree()
+ c.Assert(err, IsNil)
+
+ util.WriteFile(fs, "foo", []byte("foo\nbar\n"), 0644)
+
+ _, err = w.Add("foo")
+ c.Assert(err, IsNil)
+
+ _, err = w.Commit("foo\n", cm)
+ c.Assert(err, IsNil)
+
+ _, err = w.Move("foo", "bar")
+ c.Assert(err, IsNil)
+
+ hash, err := w.Commit("rename foo to bar", cm)
+ c.Assert(err, IsNil)
+
+ commit, err := r.CommitObject(hash)
+ c.Assert(err, IsNil)
+
+ fileStats, err := commit.Stats()
+ c.Assert(err, IsNil)
+ c.Assert(fileStats[0].Name, Equals, "foo => bar")
+}
diff --git a/plumbing/object/patch_test.go b/plumbing/object/patch_test.go
index 2cff795..e0e63a5 100644
--- a/plumbing/object/patch_test.go
+++ b/plumbing/object/patch_test.go
@@ -45,3 +45,113 @@ func (s *PatchSuite) TestStatsWithSubmodules(c *C) {
c.Assert(err, IsNil)
c.Assert(p, NotNil)
}
+
+func (s *PatchSuite) TestFileStatsString(c *C) {
+ testCases := []struct {
+ description string
+ input FileStats
+ expected string
+ }{
+
+ {
+ description: "no files changed",
+ input: []FileStat{},
+ expected: "",
+ },
+ {
+ description: "one file touched - no changes",
+ input: []FileStat{
+ {
+ Name: "file1",
+ },
+ },
+ expected: " file1 | 0 \n",
+ },
+ {
+ description: "one file changed",
+ input: []FileStat{
+ {
+ Name: "file1",
+ Addition: 1,
+ },
+ },
+ expected: " file1 | 1 +\n",
+ },
+ {
+ description: "one file changed with one addition and one deletion",
+ input: []FileStat{
+ {
+ Name: ".github/workflows/git.yml",
+ Addition: 1,
+ Deletion: 1,
+ },
+ },
+ expected: " .github/workflows/git.yml | 2 +-\n",
+ },
+ {
+ description: "two files changed",
+ input: []FileStat{
+ {
+ Name: ".github/workflows/git.yml",
+ Addition: 1,
+ Deletion: 1,
+ },
+ {
+ Name: "cli/go-git/go.mod",
+ Addition: 4,
+ Deletion: 4,
+ },
+ },
+ expected: " .github/workflows/git.yml | 2 +-\n cli/go-git/go.mod | 8 ++++----\n",
+ },
+ {
+ description: "three files changed",
+ input: []FileStat{
+ {
+ Name: ".github/workflows/git.yml",
+ Addition: 3,
+ Deletion: 3,
+ },
+ {
+ Name: "worktree.go",
+ Addition: 107,
+ },
+ {
+ Name: "worktree_test.go",
+ Addition: 75,
+ },
+ },
+ expected: " .github/workflows/git.yml | 6 +++---\n" +
+ " worktree.go | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++\n" +
+ " worktree_test.go | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++\n",
+ },
+ {
+ description: "three files changed with deletions and additions",
+ input: []FileStat{
+ {
+ Name: ".github/workflows/git.yml",
+ Addition: 3,
+ Deletion: 3,
+ },
+ {
+ Name: "worktree.go",
+ Addition: 107,
+ Deletion: 217,
+ },
+ {
+ Name: "worktree_test.go",
+ Addition: 75,
+ Deletion: 275,
+ },
+ },
+ expected: " .github/workflows/git.yml | 6 +++---\n" +
+ " worktree.go | 324 ++++++++++++++++++-----------------------------------\n" +
+ " worktree_test.go | 350 ++++++++++++-----------------------------------------\n",
+ },
+ }
+
+ for _, tc := range testCases {
+ c.Log("Executing test cases:", tc.description)
+ c.Assert(printStat(tc.input), Equals, tc.expected)
+ }
+}
diff --git a/plumbing/object/tree.go b/plumbing/object/tree.go
index e9f7666..0fd0e51 100644
--- a/plumbing/object/tree.go
+++ b/plumbing/object/tree.go
@@ -7,6 +7,7 @@ import (
"io"
"path"
"path/filepath"
+ "sort"
"strings"
"github.com/go-git/go-git/v5/plumbing"
@@ -27,6 +28,7 @@ var (
ErrFileNotFound = errors.New("file not found")
ErrDirectoryNotFound = errors.New("directory not found")
ErrEntryNotFound = errors.New("entry not found")
+ ErrEntriesNotSorted = errors.New("entries in tree are not sorted")
)
// Tree is basically like a directory - it references a bunch of other trees
@@ -270,6 +272,28 @@ func (t *Tree) Decode(o plumbing.EncodedObject) (err error) {
return nil
}
+type TreeEntrySorter []TreeEntry
+
+func (s TreeEntrySorter) Len() int {
+ return len(s)
+}
+
+func (s TreeEntrySorter) Less(i, j int) bool {
+ name1 := s[i].Name
+ name2 := s[j].Name
+ if s[i].Mode == filemode.Dir {
+ name1 += "/"
+ }
+ if s[j].Mode == filemode.Dir {
+ name2 += "/"
+ }
+ return name1 < name2
+}
+
+func (s TreeEntrySorter) Swap(i, j int) {
+ s[i], s[j] = s[j], s[i]
+}
+
// Encode transforms a Tree into a plumbing.EncodedObject.
func (t *Tree) Encode(o plumbing.EncodedObject) (err error) {
o.SetType(plumbing.TreeObject)
@@ -279,7 +303,15 @@ func (t *Tree) Encode(o plumbing.EncodedObject) (err error) {
}
defer ioutil.CheckClose(w, &err)
+
+ if !sort.IsSorted(TreeEntrySorter(t.Entries)) {
+ return ErrEntriesNotSorted
+ }
+
for _, entry := range t.Entries {
+ if strings.IndexByte(entry.Name, 0) != -1 {
+ return fmt.Errorf("malformed filename %q", entry.Name)
+ }
if _, err = fmt.Fprintf(w, "%o %s", entry.Mode, entry.Name); err != nil {
return err
}
diff --git a/plumbing/object/tree_test.go b/plumbing/object/tree_test.go
index bb5fc7a..feb058a 100644
--- a/plumbing/object/tree_test.go
+++ b/plumbing/object/tree_test.go
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"io"
+ "sort"
"testing"
fixtures "github.com/go-git/go-git-fixtures/v4"
@@ -220,6 +221,30 @@ func (o *SortReadCloser) Read(p []byte) (int, error) {
return nw, nil
}
+func (s *TreeSuite) TestTreeEntriesSorted(c *C) {
+ tree := &Tree{
+ Entries: []TreeEntry{
+ {"foo", filemode.Empty, plumbing.NewHash("b029517f6300c2da0f4b651b8642506cd6aaf45d")},
+ {"bar", filemode.Empty, plumbing.NewHash("c029517f6300c2da0f4b651b8642506cd6aaf45d")},
+ {"baz", filemode.Empty, plumbing.NewHash("d029517f6300c2da0f4b651b8642506cd6aaf45d")},
+ },
+ }
+
+ {
+ c.Assert(sort.IsSorted(TreeEntrySorter(tree.Entries)), Equals, false)
+ obj := &plumbing.MemoryObject{}
+ err := tree.Encode(obj)
+ c.Assert(err, Equals, ErrEntriesNotSorted)
+ }
+
+ {
+ sort.Sort(TreeEntrySorter(tree.Entries))
+ obj := &plumbing.MemoryObject{}
+ err := tree.Encode(obj)
+ c.Assert(err, IsNil)
+ }
+}
+
func (s *TreeSuite) TestTreeDecodeEncodeIdempotent(c *C) {
trees := []*Tree{
{
@@ -231,6 +256,7 @@ func (s *TreeSuite) TestTreeDecodeEncodeIdempotent(c *C) {
},
}
for _, tree := range trees {
+ sort.Sort(TreeEntrySorter(tree.Entries))
obj := &plumbing.MemoryObject{}
err := tree.Encode(obj)
c.Assert(err, IsNil)
diff --git a/plumbing/object/treenoder.go b/plumbing/object/treenoder.go
index 6e7b334..2adb645 100644
--- a/plumbing/object/treenoder.go
+++ b/plumbing/object/treenoder.go
@@ -88,7 +88,9 @@ func (t *treeNoder) Children() ([]noder.Noder, error) {
}
}
- return transformChildren(parent)
+ var err error
+ t.children, err = transformChildren(parent)
+ return t.children, err
}
// Returns the children of a tree as treenoders.