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
10// NewFilterCommitIter returns a CommitIter that walks the commit history,
11// starting at the passed commit and visiting its parents in Breadth-first order.
12// The commits returned by the CommitIter will validate the passed CommitFilter.
13// The history won't be transversed beyond a commit if isLimit is true for it.
14// Each commit will be visited only once.
15// If the commit history can not be traversed, or the Close() method is called,
16// the CommitIter won't return more commits.
17// If no isValid is passed, all ancestors of from commit will be valid.
18// If no isLimit is limit, all ancestors of all commits will be visited.
19func NewFilterCommitIter(
20	from *Commit,
21	isValid *CommitFilter,
22	isLimit *CommitFilter,
23) CommitIter {
24	var validFilter CommitFilter
25	if isValid == nil {
26		validFilter = func(_ *Commit) bool {
27			return true
28		}
29	} else {
30		validFilter = *isValid
31	}
32
33	var limitFilter CommitFilter
34	if isLimit == nil {
35		limitFilter = func(_ *Commit) bool {
36			return false
37		}
38	} else {
39		limitFilter = *isLimit
40	}
41
42	return &filterCommitIter{
43		isValid: validFilter,
44		isLimit: limitFilter,
45		visited: map[plumbing.Hash]struct{}{},
46		queue:   []*Commit{from},
47	}
48}
49
50// CommitFilter returns a boolean for the passed Commit
51type CommitFilter func(*Commit) bool
52
53// filterCommitIter implements CommitIter
54type filterCommitIter struct {
55	isValid CommitFilter
56	isLimit CommitFilter
57	visited map[plumbing.Hash]struct{}
58	queue   []*Commit
59	lastErr error
60}
61
62// Next returns the next commit of the CommitIter.
63// It will return io.EOF if there are no more commits to visit,
64// or an error if the history could not be traversed.
65func (w *filterCommitIter) Next() (*Commit, error) {
66	var commit *Commit
67	var err error
68	for {
69		commit, err = w.popNewFromQueue()
70		if err != nil {
71			return nil, w.close(err)
72		}
73
74		w.visited[commit.Hash] = struct{}{}
75
76		if !w.isLimit(commit) {
77			err = w.addToQueue(commit.s, commit.ParentHashes...)
78			if err != nil {
79				return nil, w.close(err)
80			}
81		}
82
83		if w.isValid(commit) {
84			return commit, nil
85		}
86	}
87}
88
89// ForEach runs the passed callback over each Commit returned by the CommitIter
90// until the callback returns an error or there is no more commits to traverse.
91func (w *filterCommitIter) ForEach(cb func(*Commit) error) error {
92	for {
93		commit, err := w.Next()
94		if err == io.EOF {
95			break
96		}
97
98		if err != nil {
99			return err
100		}
101
102		if err := cb(commit); err == storer.ErrStop {
103			break
104		} else if err != nil {
105			return err
106		}
107	}
108
109	return nil
110}
111
112// Error returns the error that caused that the CommitIter is no longer returning commits
113func (w *filterCommitIter) Error() error {
114	return w.lastErr
115}
116
117// Close closes the CommitIter
118func (w *filterCommitIter) Close() {
119	w.visited = map[plumbing.Hash]struct{}{}
120	w.queue = []*Commit{}
121	w.isLimit = nil
122	w.isValid = nil
123}
124
125// close closes the CommitIter with an error
126func (w *filterCommitIter) close(err error) error {
127	w.Close()
128	w.lastErr = err
129	return err
130}
131
132// popNewFromQueue returns the first new commit from the internal fifo queue,
133// or an io.EOF error if the queue is empty
134func (w *filterCommitIter) popNewFromQueue() (*Commit, error) {
135	var first *Commit
136	for {
137		if len(w.queue) == 0 {
138			if w.lastErr != nil {
139				return nil, w.lastErr
140			}
141
142			return nil, io.EOF
143		}
144
145		first = w.queue[0]
146		w.queue = w.queue[1:]
147		if _, ok := w.visited[first.Hash]; ok {
148			continue
149		}
150
151		return first, nil
152	}
153}
154
155// addToQueue adds the passed commits to the internal fifo queue if they weren't seen
156// or returns an error if the passed hashes could not be used to get valid commits
157func (w *filterCommitIter) addToQueue(
158	store storer.EncodedObjectStorer,
159	hashes ...plumbing.Hash,
160) error {
161	for _, hash := range hashes {
162		if _, ok := w.visited[hash]; ok {
163			continue
164		}
165
166		commit, err := GetCommit(store, hash)
167		if err != nil {
168			return err
169		}
170
171		w.queue = append(w.queue, commit)
172	}
173
174	return nil
175}
176