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

Commit 52fcf7d

Browse files
authored
Merge pull request #1132 from filipnavara/commitgraph-obj
plumbing: object, add APIs for traversing over commit graphs
2 parents e17ee11 + d2596b8 commit 52fcf7d

File tree

12 files changed

+961
-6
lines changed

12 files changed

+961
-6
lines changed

_examples/common_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ var args = map[string][]string{
2828
"showcase": {defaultURL, tempFolder()},
2929
"tag": {cloneRepository(defaultURL, tempFolder())},
3030
"pull": {createRepositoryWithRemote(tempFolder(), defaultURL)},
31+
"ls": {cloneRepository(defaultURL, tempFolder()), "HEAD", "vendor"},
3132
}
3233

3334
var ignored = map[string]bool{}

_examples/ls/main.go

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

plumbing/format/commitgraph/commitgraph_test.go

+4-6
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@ import (
66
"path"
77
"testing"
88

9-
"golang.org/x/exp/mmap"
10-
119
. "gopkg.in/check.v1"
12-
"gopkg.in/src-d/go-git-fixtures.v3"
10+
fixtures "gopkg.in/src-d/go-git-fixtures.v3"
1311
"gopkg.in/src-d/go-git.v4/plumbing"
1412
"gopkg.in/src-d/go-git.v4/plumbing/format/commitgraph"
1513
)
@@ -23,7 +21,7 @@ type CommitgraphSuite struct {
2321
var _ = Suite(&CommitgraphSuite{})
2422

2523
func testDecodeHelper(c *C, path string) {
26-
reader, err := mmap.Open(path)
24+
reader, err := os.Open(path)
2725
c.Assert(err, IsNil)
2826
defer reader.Close()
2927
index, err := commitgraph.OpenFileIndex(reader)
@@ -85,7 +83,7 @@ func (s *CommitgraphSuite) TestReencode(c *C) {
8583
fixtures.ByTag("commit-graph").Test(c, func(f *fixtures.Fixture) {
8684
dotgit := f.DotGit()
8785

88-
reader, err := mmap.Open(path.Join(dotgit.Root(), "objects", "info", "commit-graph"))
86+
reader, err := os.Open(path.Join(dotgit.Root(), "objects", "info", "commit-graph"))
8987
c.Assert(err, IsNil)
9088
defer reader.Close()
9189
index, err := commitgraph.OpenFileIndex(reader)
@@ -108,7 +106,7 @@ func (s *CommitgraphSuite) TestReencodeInMemory(c *C) {
108106
fixtures.ByTag("commit-graph").Test(c, func(f *fixtures.Fixture) {
109107
dotgit := f.DotGit()
110108

111-
reader, err := mmap.Open(path.Join(dotgit.Root(), "objects", "info", "commit-graph"))
109+
reader, err := os.Open(path.Join(dotgit.Root(), "objects", "info", "commit-graph"))
112110
c.Assert(err, IsNil)
113111
index, err := commitgraph.OpenFileIndex(reader)
114112
c.Assert(err, IsNil)

0 commit comments

Comments
 (0)