Skip to content
This repository was archived by the owner on Sep 11, 2020. It is now read-only.

Commit 944861a

Browse files
committed
Add example for commit-graph traversal
Signed-off-by: Filip Navara <filip.navara@gmail.com>
1 parent 7d26957 commit 944861a

File tree

1 file changed

+268
-0
lines changed

1 file changed

+268
-0
lines changed

_examples/ls/main.go

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
"path"
8+
"strings"
9+
10+
"github.com/emirpasic/gods/trees/binaryheap"
11+
"gopkg.in/src-d/go-git.v4"
12+
. "gopkg.in/src-d/go-git.v4/_examples"
13+
"gopkg.in/src-d/go-git.v4/plumbing"
14+
"gopkg.in/src-d/go-git.v4/plumbing/cache"
15+
commitgraph_fmt "gopkg.in/src-d/go-git.v4/plumbing/format/commitgraph"
16+
"gopkg.in/src-d/go-git.v4/plumbing/object"
17+
"gopkg.in/src-d/go-git.v4/plumbing/object/commitgraph"
18+
"gopkg.in/src-d/go-git.v4/storage/filesystem"
19+
20+
"gopkg.in/src-d/go-billy.v4"
21+
"gopkg.in/src-d/go-billy.v4/osfs"
22+
)
23+
24+
// Example how to resolve a revision into its commit counterpart
25+
func main() {
26+
CheckArgs("<path>", "<revision>", "<tree path>")
27+
28+
path := os.Args[1]
29+
revision := os.Args[2]
30+
treePath := os.Args[3]
31+
32+
// We instantiate a new repository targeting the given path (the .git folder)
33+
fs := osfs.New(path)
34+
s := filesystem.NewStorageWithOptions(fs, cache.NewObjectLRUDefault(), filesystem.Options{KeepDescriptors: true})
35+
r, err := git.Open(s, fs)
36+
CheckIfError(err)
37+
38+
// Resolve revision into a sha1 commit, only some revisions are resolved
39+
// look at the doc to get more details
40+
Info("git rev-parse %s", revision)
41+
42+
h, err := r.ResolveRevision(plumbing.Revision(revision))
43+
CheckIfError(err)
44+
45+
commit, err := r.CommitObject(*h)
46+
CheckIfError(err)
47+
48+
tree, err := commit.Tree()
49+
CheckIfError(err)
50+
if treePath != "" {
51+
tree, err = tree.Tree(treePath)
52+
CheckIfError(err)
53+
}
54+
55+
var paths []string
56+
for _, entry := range tree.Entries {
57+
paths = append(paths, entry.Name)
58+
}
59+
60+
commitNodeIndex, file := getCommitNodeIndex(r, fs)
61+
if file != nil {
62+
defer file.Close()
63+
}
64+
65+
commitNode, err := commitNodeIndex.Get(*h)
66+
CheckIfError(err)
67+
68+
revs, err := getLastCommitForPaths(commitNode, treePath, paths)
69+
CheckIfError(err)
70+
for path, rev := range revs {
71+
// Print one line per file (name hash message)
72+
hash := rev.Hash.String()
73+
line := strings.Split(rev.Message, "\n")
74+
fmt.Println(path, hash[:7], line[0])
75+
}
76+
77+
s.Close()
78+
}
79+
80+
func getCommitNodeIndex(r *git.Repository, fs billy.Filesystem) (commitgraph.CommitNodeIndex, io.ReadCloser) {
81+
file, err := fs.Open(path.Join("objects", "info", "commit-graph"))
82+
if err == nil {
83+
index, err := commitgraph_fmt.OpenFileIndex(file)
84+
if err == nil {
85+
return commitgraph.NewGraphCommitNodeIndex(index, r.Storer), file
86+
}
87+
file.Close()
88+
}
89+
90+
return commitgraph.NewObjectCommitNodeIndex(r.Storer), nil
91+
}
92+
93+
type commitAndPaths struct {
94+
commit commitgraph.CommitNode
95+
// Paths that are still on the branch represented by commit
96+
paths []string
97+
// Set of hashes for the paths
98+
hashes map[string]plumbing.Hash
99+
}
100+
101+
func getCommitTree(c commitgraph.CommitNode, treePath string) (*object.Tree, error) {
102+
tree, err := c.Tree()
103+
if err != nil {
104+
return nil, err
105+
}
106+
107+
// Optimize deep traversals by focusing only on the specific tree
108+
if treePath != "" {
109+
tree, err = tree.Tree(treePath)
110+
if err != nil {
111+
return nil, err
112+
}
113+
}
114+
115+
return tree, nil
116+
}
117+
118+
func getFullPath(treePath, path string) string {
119+
if treePath != "" {
120+
if path != "" {
121+
return treePath + "/" + path
122+
}
123+
return treePath
124+
}
125+
return path
126+
}
127+
128+
func getFileHashes(c commitgraph.CommitNode, treePath string, paths []string) (map[string]plumbing.Hash, error) {
129+
tree, err := getCommitTree(c, treePath)
130+
if err == object.ErrDirectoryNotFound {
131+
// The whole tree didn't exist, so return empty map
132+
return make(map[string]plumbing.Hash), nil
133+
}
134+
if err != nil {
135+
return nil, err
136+
}
137+
138+
hashes := make(map[string]plumbing.Hash)
139+
for _, path := range paths {
140+
if path != "" {
141+
entry, err := tree.FindEntry(path)
142+
if err == nil {
143+
hashes[path] = entry.Hash
144+
}
145+
} else {
146+
hashes[path] = tree.Hash
147+
}
148+
}
149+
150+
return hashes, nil
151+
}
152+
153+
func getLastCommitForPaths(c commitgraph.CommitNode, treePath string, paths []string) (map[string]*object.Commit, error) {
154+
// We do a tree traversal with nodes sorted by commit time
155+
heap := binaryheap.NewWith(func(a, b interface{}) int {
156+
if a.(*commitAndPaths).commit.CommitTime().Before(b.(*commitAndPaths).commit.CommitTime()) {
157+
return 1
158+
}
159+
return -1
160+
})
161+
162+
resultNodes := make(map[string]commitgraph.CommitNode)
163+
initialHashes, err := getFileHashes(c, treePath, paths)
164+
if err != nil {
165+
return nil, err
166+
}
167+
168+
// Start search from the root commit and with full set of paths
169+
heap.Push(&commitAndPaths{c, paths, initialHashes})
170+
171+
for {
172+
cIn, ok := heap.Pop()
173+
if !ok {
174+
break
175+
}
176+
current := cIn.(*commitAndPaths)
177+
178+
// Load the parent commits for the one we are currently examining
179+
numParents := current.commit.NumParents()
180+
var parents []commitgraph.CommitNode
181+
for i := 0; i < numParents; i++ {
182+
parent, err := current.commit.ParentNode(i)
183+
if err != nil {
184+
break
185+
}
186+
parents = append(parents, parent)
187+
}
188+
189+
// Examine the current commit and set of interesting paths
190+
pathUnchanged := make([]bool, len(current.paths))
191+
parentHashes := make([]map[string]plumbing.Hash, len(parents))
192+
for j, parent := range parents {
193+
parentHashes[j], err = getFileHashes(parent, treePath, current.paths)
194+
if err != nil {
195+
break
196+
}
197+
198+
for i, path := range current.paths {
199+
if parentHashes[j][path] == current.hashes[path] {
200+
pathUnchanged[i] = true
201+
}
202+
}
203+
}
204+
205+
var remainingPaths []string
206+
for i, path := range current.paths {
207+
// The results could already contain some newer change for the same path,
208+
// so don't override that and bail out on the file early.
209+
if resultNodes[path] == nil {
210+
if pathUnchanged[i] {
211+
// The path existed with the same hash in at least one parent so it could
212+
// not have been changed in this commit directly.
213+
remainingPaths = append(remainingPaths, path)
214+
} else {
215+
// There are few possible cases how can we get here:
216+
// - The path didn't exist in any parent, so it must have been created by
217+
// this commit.
218+
// - The path did exist in the parent commit, but the hash of the file has
219+
// changed.
220+
// - We are looking at a merge commit and the hash of the file doesn't
221+
// match any of the hashes being merged. This is more common for directories,
222+
// but it can also happen if a file is changed through conflict resolution.
223+
resultNodes[path] = current.commit
224+
}
225+
}
226+
}
227+
228+
if len(remainingPaths) > 0 {
229+
// Add the parent nodes along with remaining paths to the heap for further
230+
// processing.
231+
for j, parent := range parents {
232+
// Combine remainingPath with paths available on the parent branch
233+
// and make union of them
234+
remainingPathsForParent := make([]string, 0, len(remainingPaths))
235+
newRemainingPaths := make([]string, 0, len(remainingPaths))
236+
for _, path := range remainingPaths {
237+
if parentHashes[j][path] == current.hashes[path] {
238+
remainingPathsForParent = append(remainingPathsForParent, path)
239+
} else {
240+
newRemainingPaths = append(newRemainingPaths, path)
241+
}
242+
}
243+
244+
if remainingPathsForParent != nil {
245+
heap.Push(&commitAndPaths{parent, remainingPathsForParent, parentHashes[j]})
246+
}
247+
248+
if len(newRemainingPaths) == 0 {
249+
break
250+
} else {
251+
remainingPaths = newRemainingPaths
252+
}
253+
}
254+
}
255+
}
256+
257+
// Post-processing
258+
result := make(map[string]*object.Commit)
259+
for path, commitNode := range resultNodes {
260+
var err error
261+
result[path], err = commitNode.Commit()
262+
if err != nil {
263+
return nil, err
264+
}
265+
}
266+
267+
return result, nil
268+
}

0 commit comments

Comments
 (0)