1package object
2
3import (
4 "io"
5
6 "github.com/go-git/go-git/v5/plumbing"
7 "github.com/go-git/go-git/v5/plumbing/storer"
8)
9
10type commitPathIter struct {
11 pathFilter func(string) bool
12 sourceIter CommitIter
13 currentCommit *Commit
14 checkParent bool
15}
16
17// NewCommitPathIterFromIter returns a commit iterator which performs diffTree between
18// successive trees returned from the commit iterator from the argument. The purpose of this is
19// to find the commits that explain how the files that match the path came to be.
20// If checkParent is true then the function double checks if potential parent (next commit in a path)
21// is one of the parents in the tree (it's used by `git log --all`).
22// pathFilter is a function that takes path of file as argument and returns true if we want it
23func NewCommitPathIterFromIter(pathFilter func(string) bool, commitIter CommitIter, checkParent bool) CommitIter {
24 iterator := new(commitPathIter)
25 iterator.sourceIter = commitIter
26 iterator.pathFilter = pathFilter
27 iterator.checkParent = checkParent
28 return iterator
29}
30
31// NewCommitFileIterFromIter is kept for compatibility, can be replaced with NewCommitPathIterFromIter
32func NewCommitFileIterFromIter(fileName string, commitIter CommitIter, checkParent bool) CommitIter {
33 return NewCommitPathIterFromIter(
34 func(path string) bool {
35 return path == fileName
36 },
37 commitIter,
38 checkParent,
39 )
40}
41
42func (c *commitPathIter) Next() (*Commit, error) {
43 if c.currentCommit == nil {
44 var err error
45 c.currentCommit, err = c.sourceIter.Next()
46 if err != nil {
47 return nil, err
48 }
49 }
50 commit, commitErr := c.getNextFileCommit()
51
52 // Setting current-commit to nil to prevent unwanted states when errors are raised
53 if commitErr != nil {
54 c.currentCommit = nil
55 }
56 return commit, commitErr
57}
58
59func (c *commitPathIter) getNextFileCommit() (*Commit, error) {
60 var parentTree, currentTree *Tree
61
62 for {
63 // Parent-commit can be nil if the current-commit is the initial commit
64 parentCommit, parentCommitErr := c.sourceIter.Next()
65 if parentCommitErr != nil {
66 // If the parent-commit is beyond the initial commit, keep it nil
67 if parentCommitErr != io.EOF {
68 return nil, parentCommitErr
69 }
70 parentCommit = nil
71 }
72
73 if parentTree == nil {
74 var currTreeErr error
75 currentTree, currTreeErr = c.currentCommit.Tree()
76 if currTreeErr != nil {
77 return nil, currTreeErr
78 }
79 } else {
80 currentTree = parentTree
81 parentTree = nil
82 }
83
84 if parentCommit != nil {
85 var parentTreeErr error
86 parentTree, parentTreeErr = parentCommit.Tree()
87 if parentTreeErr != nil {
88 return nil, parentTreeErr
89 }
90 }
91
92 // Find diff between current and parent trees
93 changes, diffErr := DiffTree(currentTree, parentTree)
94 if diffErr != nil {
95 return nil, diffErr
96 }
97
98 found := c.hasFileChange(changes, parentCommit)
99
100 // Storing the current-commit in-case a change is found, and
101 // Updating the current-commit for the next-iteration
102 prevCommit := c.currentCommit
103 c.currentCommit = parentCommit
104
105 if found {
106 return prevCommit, nil
107 }
108
109 // If not matches found and if parent-commit is beyond the initial commit, then return with EOF
110 if parentCommit == nil {
111 return nil, io.EOF
112 }
113 }
114}
115
116func (c *commitPathIter) hasFileChange(changes Changes, parent *Commit) bool {
117 for _, change := range changes {
118 if !c.pathFilter(change.name()) {
119 continue
120 }
121
122 // filename matches, now check if source iterator contains all commits (from all refs)
123 if c.checkParent {
124 // Check if parent is beyond the initial commit
125 if parent == nil || isParentHash(parent.Hash, c.currentCommit) {
126 return true
127 }
128 continue
129 }
130
131 return true
132 }
133
134 return false
135}
136
137func isParentHash(hash plumbing.Hash, commit *Commit) bool {
138 for _, h := range commit.ParentHashes {
139 if h == hash {
140 return true
141 }
142 }
143 return false
144}
145
146func (c *commitPathIter) ForEach(cb func(*Commit) error) error {
147 for {
148 commit, nextErr := c.Next()
149 if nextErr == io.EOF {
150 break
151 }
152 if nextErr != nil {
153 return nextErr
154 }
155 err := cb(commit)
156 if err == storer.ErrStop {
157 return nil
158 } else if err != nil {
159 return err
160 }
161 }
162 return nil
163}
164
165func (c *commitPathIter) Close() {
166 c.sourceIter.Close()
167}