diff options
Diffstat (limited to 'plumbing/object')
-rw-r--r-- | plumbing/object/commit.go | 6 | ||||
-rw-r--r-- | plumbing/object/commit_test.go | 8 | ||||
-rw-r--r-- | plumbing/object/commit_walker_path.go | 19 | ||||
-rw-r--r-- | plumbing/object/commit_walker_test.go | 26 | ||||
-rw-r--r-- | plumbing/object/patch.go | 99 | ||||
-rw-r--r-- | plumbing/object/patch_stats_test.go | 54 | ||||
-rw-r--r-- | plumbing/object/patch_test.go | 110 | ||||
-rw-r--r-- | plumbing/object/tree.go | 32 | ||||
-rw-r--r-- | plumbing/object/tree_test.go | 26 | ||||
-rw-r--r-- | plumbing/object/treenoder.go | 4 |
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. |