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

Commit aebe980

Browse files
committed
WIP
1 parent 930ff6a commit aebe980

File tree

4 files changed

+435
-0
lines changed

4 files changed

+435
-0
lines changed
+235
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
package commitgraph
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"io"
7+
"math"
8+
"time"
9+
10+
"gopkg.in/src-d/go-git.v4/plumbing"
11+
"gopkg.in/src-d/go-git.v4/utils/binary"
12+
)
13+
14+
// Node is a reduced representation of Commit as presented in the commit graph
15+
// file. It is merely useful as an optimization for walking the commit graphs.
16+
type Node struct {
17+
// TreeHash is the hash of the root tree of the commit.
18+
TreeHash plumbing.Hash
19+
// ParentHashes are the hashes of the parent commits of the commit.
20+
ParentIndexes []uint32
21+
// Generation number is the pre-computed generation in the commit graph
22+
// or zero if not available
23+
Generation uint32
24+
// When is the timestamp of the commit.
25+
When time.Time
26+
}
27+
28+
// Index represents a representation of commit graph that allows indexed
29+
// access to the nodes using commit object hash
30+
type Index interface {
31+
// GetIndexByHash gets the index in the commit graph from commit hash, if available
32+
GetIndexByHash(h plumbing.Hash) (uint32, error)
33+
// GetHashByIndex gets the hash from node index, if available
34+
GetHashByIndex(i uint32) (plumbing.Hash, error)
35+
// GetNodeByIndex gets the commit node from the commit graph using index
36+
// obtained from child node, if available
37+
GetNodeByIndex(i uint32) (*Node, error)
38+
}
39+
40+
var (
41+
// ErrUnsupportedVersion is returned by OpenFileIndex when the commit graph
42+
// file version is not supported.
43+
ErrUnsupportedVersion = errors.New("Unsuported version")
44+
// ErrUnsupportedHash is returned by OpenFileIndex when the commit graph
45+
// hash function is not supported. Currently only SHA-1 is defined and
46+
// supported
47+
ErrUnsupportedHash = errors.New("Unsuported hash algorithm")
48+
// ErrMalformedCommitGraphFile is returned by OpenFileIndex when the commit
49+
// graph file is corrupted.
50+
ErrMalformedCommitGraphFile = errors.New("Malformed commit graph file")
51+
52+
commitFileSignature = []byte{'C', 'G', 'P', 'H'}
53+
oidFanoutSignature = []byte{'O', 'I', 'D', 'F'}
54+
oidLookupSignature = []byte{'O', 'I', 'D', 'L'}
55+
commitDataSignature = []byte{'C', 'D', 'A', 'T'}
56+
largeEdgeListSignature = []byte{'E', 'D', 'G', 'E'}
57+
58+
parentNone = uint32(0x70000000)
59+
parentOctopusUsed = uint32(0x80000000)
60+
parentOctopusMask = uint32(0x7fffffff)
61+
parentLast = uint32(0x80000000)
62+
)
63+
64+
type fileIndex struct {
65+
reader io.ReaderAt
66+
fanout [256]uint32
67+
oidLookupOffset int64
68+
commitDataOffset int64
69+
largeEdgeListOffset int64
70+
}
71+
72+
// OpenFileIndex opens a serialized commit graph file
73+
// in the format described at
74+
// https://github.com/git/git/blob/master/Documentation/technical/commit-graph-format.txt
75+
func OpenFileIndex(reader io.ReaderAt) (Index, error) {
76+
// Verify file signature
77+
var signature = make([]byte, 4)
78+
if _, err := reader.ReadAt(signature, 0); err != nil {
79+
return nil, err
80+
}
81+
if !bytes.Equal(signature, commitFileSignature) {
82+
return nil, ErrMalformedCommitGraphFile
83+
}
84+
85+
// Read and verify the file header
86+
var header = make([]byte, 4)
87+
if _, err := reader.ReadAt(header, 4); err != nil {
88+
return nil, err
89+
}
90+
if header[0] != 1 {
91+
return nil, ErrUnsupportedVersion
92+
}
93+
if header[1] != 1 {
94+
return nil, ErrUnsupportedHash
95+
}
96+
97+
// Read chunk headers
98+
var chunkID = make([]byte, 4)
99+
var oidFanoutOffset int64
100+
var oidLookupOffset int64
101+
var commitDataOffset int64
102+
var largeEdgeListOffset int64
103+
chunkCount := int(header[2])
104+
for i := 0; i < chunkCount; i++ {
105+
chunkHeader := io.NewSectionReader(reader, 8+(int64(i)*12), 12)
106+
if _, err := io.ReadAtLeast(chunkHeader, chunkID, 4); err != nil {
107+
return nil, err
108+
}
109+
chunkOffset, err := binary.ReadUint64(chunkHeader)
110+
if err != nil {
111+
return nil, err
112+
}
113+
114+
if bytes.Equal(chunkID, oidFanoutSignature) {
115+
oidFanoutOffset = int64(chunkOffset)
116+
} else if bytes.Equal(chunkID, oidLookupSignature) {
117+
oidLookupOffset = int64(chunkOffset)
118+
} else if bytes.Equal(chunkID, commitDataSignature) {
119+
commitDataOffset = int64(chunkOffset)
120+
} else if bytes.Equal(chunkID, largeEdgeListSignature) {
121+
largeEdgeListOffset = int64(chunkOffset)
122+
}
123+
}
124+
125+
if oidFanoutOffset <= 0 || oidLookupOffset <= 0 || commitDataOffset <= 0 {
126+
return nil, ErrMalformedCommitGraphFile
127+
}
128+
129+
// Read fanout table and calculate the file offsets into the lookup table
130+
fanoutReader := io.NewSectionReader(reader, oidFanoutOffset, 256*4)
131+
var fanout [256]uint32
132+
for i := 0; i < 256; i++ {
133+
var err error
134+
if fanout[i], err = binary.ReadUint32(fanoutReader); err != nil {
135+
return nil, err
136+
}
137+
}
138+
139+
return &fileIndex{reader, fanout, oidLookupOffset, commitDataOffset, largeEdgeListOffset}, nil
140+
}
141+
142+
func (fi *fileIndex) GetIndexByHash(h plumbing.Hash) (uint32, error) {
143+
var oid plumbing.Hash
144+
145+
// Find the hash in the oid lookup table
146+
var low uint32
147+
if h[0] == 0 {
148+
low = 0
149+
} else {
150+
low = fi.fanout[h[0]-1]
151+
}
152+
high := fi.fanout[h[0]]
153+
for low < high {
154+
mid := (low + high) >> 1
155+
offset := fi.oidLookupOffset + int64(mid)*20
156+
if _, err := fi.reader.ReadAt(oid[:], offset); err != nil {
157+
return 0, err
158+
}
159+
cmp := bytes.Compare(h[:], oid[:])
160+
if cmp < 0 {
161+
high = mid
162+
} else if cmp == 0 {
163+
return mid, nil
164+
} else {
165+
low = mid + 1
166+
}
167+
}
168+
169+
return 0, plumbing.ErrObjectNotFound
170+
}
171+
172+
func (fi *fileIndex) GetNodeByIndex(idx uint32) (*Node, error) {
173+
offset := fi.commitDataOffset + int64(idx)*36
174+
commitDataReader := io.NewSectionReader(fi.reader, offset, 36)
175+
176+
treeHash, err := binary.ReadHash(commitDataReader)
177+
if err != nil {
178+
return nil, err
179+
}
180+
parent1, err := binary.ReadUint32(commitDataReader)
181+
if err != nil {
182+
return nil, err
183+
}
184+
parent2, err := binary.ReadUint32(commitDataReader)
185+
if err != nil {
186+
return nil, err
187+
}
188+
genAndTime, err := binary.ReadUint64(commitDataReader)
189+
if err != nil {
190+
return nil, err
191+
}
192+
193+
var parentIndexes []uint32
194+
if parent2&parentOctopusUsed == parentOctopusUsed {
195+
// Octopus merge
196+
parentIndexes = []uint32{parent1}
197+
offset := fi.largeEdgeListOffset + 4*int64(parent2&parentOctopusMask)
198+
parentReader := io.NewSectionReader(fi.reader, offset, math.MaxInt64)
199+
for {
200+
parent, err := binary.ReadUint32(parentReader)
201+
if err != nil {
202+
return nil, err
203+
}
204+
parentIndexes = append(parentIndexes, parent1&parentOctopusMask)
205+
if parent&parentLast == parentLast {
206+
break
207+
}
208+
}
209+
} else if parent2 != parentNone {
210+
parentIndexes = []uint32{parent1, parent2}
211+
} else if parent1 != parentNone {
212+
parentIndexes = []uint32{parent1}
213+
}
214+
215+
return &Node{
216+
TreeHash: treeHash,
217+
ParentIndexes: parentIndexes,
218+
Generation: uint32(genAndTime >> 34),
219+
When: time.Unix(int64(genAndTime&0x3FFFFFFFF), 0),
220+
}, nil
221+
}
222+
223+
func (fi *fileIndex) GetHashByIndex(i uint32) (plumbing.Hash, error) {
224+
if i > fi.fanout[0xff] {
225+
return plumbing.ZeroHash, plumbing.ErrObjectNotFound
226+
}
227+
228+
var oid plumbing.Hash
229+
offset := fi.oidLookupOffset + int64(i)*20
230+
if _, err := fi.reader.ReadAt(oid[:], offset); err != nil {
231+
return plumbing.ZeroHash, err
232+
}
233+
234+
return oid, nil
235+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package commitgraph_test
2+
3+
import (
4+
"testing"
5+
6+
"golang.org/x/exp/mmap"
7+
8+
. "gopkg.in/check.v1"
9+
"gopkg.in/src-d/go-git-fixtures.v3"
10+
"gopkg.in/src-d/go-git.v4/plumbing"
11+
"gopkg.in/src-d/go-git.v4/plumbing/format/commitgraph"
12+
)
13+
14+
func Test(t *testing.T) { TestingT(t) }
15+
16+
type CommitgraphSuite struct {
17+
fixtures.Suite
18+
}
19+
20+
var _ = Suite(&CommitgraphSuite{})
21+
22+
func (s *CommitgraphSuite) TestDecode(c *C) {
23+
reader, err := mmap.Open("C:\\Projects\\testgit\\.git\\objects\\info\\commit-graph")
24+
c.Assert(err, IsNil)
25+
index, err := commitgraph.OpenFileIndex(reader)
26+
c.Assert(err, IsNil)
27+
28+
nodeIndex, err := index.GetIndexByHash(plumbing.NewHash("5aa811d3c2f6d5d6e928a4acacd15248928c26d0"))
29+
c.Assert(err, IsNil)
30+
node, err := index.GetNodeByIndex(nodeIndex)
31+
c.Assert(err, IsNil)
32+
c.Assert(len(node.ParentIndexes), Equals, 0)
33+
34+
reader.Close()
35+
}
5.51 MB
Binary file not shown.

0 commit comments

Comments
 (0)