aboutsummaryrefslogtreecommitdiffstats
path: root/plumbing/object/commit_walker_file.go
blob: 1af9ec1160cfebdd87c35ec0f84e4e5bb48a31c6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
package object

import (
	"io"

	"gopkg.in/src-d/go-git.v4/plumbing/storer"
)

type commitFileIter struct {
	fileName      string
	sourceIter    CommitIter
	currentCommit *Commit
	all           bool
}

// NewCommitFileIterFromIter returns a commit iterator which performs diffTree between
// successive trees returned from the commit iterator from the argument. The purpose of this is
// to find the commits that explain how the files that match the path came to be.
func NewCommitFileIterFromIter(fileName string, commitIter CommitIter, all bool) CommitIter {
	iterator := new(commitFileIter)
	iterator.sourceIter = commitIter
	iterator.fileName = fileName
	iterator.all = all
	return iterator
}

func (c *commitFileIter) Next() (*Commit, error) {
	if c.currentCommit == nil {
		var err error
		c.currentCommit, err = c.sourceIter.Next()
		if err != nil {
			return nil, err
		}
	}
	commit, commitErr := c.getNextFileCommit()

	// Setting current-commit to nil to prevent unwanted states when errors are raised
	if commitErr != nil {
		c.currentCommit = nil
	}
	return commit, commitErr
}

func (c *commitFileIter) getNextFileCommit() (*Commit, error) {
	for {
		// Parent-commit can be nil if the current-commit is the initial commit
		parentCommit, parentCommitErr := c.sourceIter.Next()
		if parentCommitErr != nil {
			// If the parent-commit is beyond the initial commit, keep it nil
			if parentCommitErr != io.EOF {
				return nil, parentCommitErr
			}
			parentCommit = nil
		}

		// Fetch the trees of the current and parent commits
		currentTree, currTreeErr := c.currentCommit.Tree()
		if currTreeErr != nil {
			return nil, currTreeErr
		}

		var parentTree *Tree
		if parentCommit != nil {
			var parentTreeErr error
			parentTree, parentTreeErr = parentCommit.Tree()
			if parentTreeErr != nil {
				return nil, parentTreeErr
			}
		}

		// Find diff between current and parent trees
		changes, diffErr := DiffTree(currentTree, parentTree)
		if diffErr != nil {
			return nil, diffErr
		}

		foundChangeForFile := false
		for _, change := range changes {
			if change.name() != c.fileName {
				continue
			}

			// filename matches, now check if source iterator contains all commits (from all refs)
			if c.all {
				// for `git log --all` also check if the next commit comes from the same parent
				for _, h := range c.currentCommit.ParentHashes {
					if h == parentCommit.Hash {
						foundChangeForFile = true
						break
					}
				}
			} else {
				foundChangeForFile = true
			}

			if foundChangeForFile {
				break
			}
		}

		// Storing the current-commit in-case a change is found, and
		// Updating the current-commit for the next-iteration
		prevCommit := c.currentCommit
		c.currentCommit = parentCommit

		if foundChangeForFile == true {
			return prevCommit, nil
		}

		// If not matches found and if parent-commit is beyond the initial commit, then return with EOF
		if parentCommit == nil {
			return nil, io.EOF
		}
	}
}

func (c *commitFileIter) ForEach(cb func(*Commit) error) error {
	for {
		commit, nextErr := c.Next()
		if nextErr != nil {
			return nextErr
		}
		err := cb(commit)
		if err == storer.ErrStop {
			return nil
		} else if err != nil {
			return err
		}
	}
}

func (c *commitFileIter) Close() {
	c.sourceIter.Close()
}