From 0dd3f0c563d76e1b40448b5ff806de90711b414a Mon Sep 17 00:00:00 2001 From: Filip Navara Date: Sun, 21 Apr 2019 13:52:34 +0200 Subject: [PATCH 1/2] WIP: Experimental support for git commit graph files Signed-off-by: Filip Navara --- models/repo.go | 8 + .../plumbing/format/commitgraph/bloom.go | 92 +++++ .../format/commitgraph/commitgraph.go | 38 ++ .../format/commitgraph/commitgraph_test.go | 35 ++ .../plumbing/format/commitgraph/encoder.go | 197 +++++++++++ .../plumbing/format/commitgraph/file.go | 317 +++++++++++++++++ .../plumbing/format/commitgraph/memory.go | 78 +++++ .../commitgraph/plumbing/object/commitnode.go | 328 ++++++++++++++++++ .../object/commitnode_walker_ctime.go | 108 ++++++ .../commitgraph/tests/testgit/COMMIT_EDITMSG | 1 + modules/commitgraph/tests/testgit/FETCH_HEAD | 0 modules/commitgraph/tests/testgit/HEAD | 1 + modules/commitgraph/tests/testgit/config | 9 + modules/commitgraph/tests/testgit/description | 1 + .../tests/testgit/hooks/applypatch-msg.sample | 15 + .../tests/testgit/hooks/commit-msg.sample | 24 ++ .../testgit/hooks/fsmonitor-watchman.sample | 114 ++++++ .../tests/testgit/hooks/post-update.sample | 8 + .../tests/testgit/hooks/pre-applypatch.sample | 14 + .../tests/testgit/hooks/pre-commit.sample | 49 +++ .../tests/testgit/hooks/pre-push.sample | 53 +++ .../tests/testgit/hooks/pre-rebase.sample | 169 +++++++++ .../tests/testgit/hooks/pre-receive.sample | 24 ++ .../testgit/hooks/prepare-commit-msg.sample | 42 +++ .../tests/testgit/hooks/update.sample | 128 +++++++ modules/commitgraph/tests/testgit/index | Bin 0 -> 217 bytes .../commitgraph/tests/testgit/info/exclude | 6 + modules/commitgraph/tests/testgit/logs/HEAD | 1 + .../tests/testgit/logs/refs/heads/master | 1 + .../43/fb44daf0a8190b40cf385150627c18c164f362 | Bin 0 -> 88 bytes .../5a/a811d3c2f6d5d6e928a4acacd15248928c26d0 | Bin 0 -> 126 bytes .../d7/d0dbbbbd1f40a253ad6287e3d8f0025b27ed86 | Bin 0 -> 90 bytes .../tests/testgit/objects/info/commit-graph | Bin 0 -> 1156 bytes .../tests/testgit/refs/heads/master | 1 + modules/git/commit_info.go | 65 +++- modules/git/repo_commitgraph.go | 160 +++++++++ 36 files changed, 2074 insertions(+), 13 deletions(-) create mode 100644 modules/commitgraph/plumbing/format/commitgraph/bloom.go create mode 100644 modules/commitgraph/plumbing/format/commitgraph/commitgraph.go create mode 100644 modules/commitgraph/plumbing/format/commitgraph/commitgraph_test.go create mode 100644 modules/commitgraph/plumbing/format/commitgraph/encoder.go create mode 100644 modules/commitgraph/plumbing/format/commitgraph/file.go create mode 100644 modules/commitgraph/plumbing/format/commitgraph/memory.go create mode 100644 modules/commitgraph/plumbing/object/commitnode.go create mode 100644 modules/commitgraph/plumbing/object/commitnode_walker_ctime.go create mode 100644 modules/commitgraph/tests/testgit/COMMIT_EDITMSG create mode 100644 modules/commitgraph/tests/testgit/FETCH_HEAD create mode 100644 modules/commitgraph/tests/testgit/HEAD create mode 100644 modules/commitgraph/tests/testgit/config create mode 100644 modules/commitgraph/tests/testgit/description create mode 100644 modules/commitgraph/tests/testgit/hooks/applypatch-msg.sample create mode 100644 modules/commitgraph/tests/testgit/hooks/commit-msg.sample create mode 100644 modules/commitgraph/tests/testgit/hooks/fsmonitor-watchman.sample create mode 100644 modules/commitgraph/tests/testgit/hooks/post-update.sample create mode 100644 modules/commitgraph/tests/testgit/hooks/pre-applypatch.sample create mode 100644 modules/commitgraph/tests/testgit/hooks/pre-commit.sample create mode 100644 modules/commitgraph/tests/testgit/hooks/pre-push.sample create mode 100644 modules/commitgraph/tests/testgit/hooks/pre-rebase.sample create mode 100644 modules/commitgraph/tests/testgit/hooks/pre-receive.sample create mode 100644 modules/commitgraph/tests/testgit/hooks/prepare-commit-msg.sample create mode 100644 modules/commitgraph/tests/testgit/hooks/update.sample create mode 100644 modules/commitgraph/tests/testgit/index create mode 100644 modules/commitgraph/tests/testgit/info/exclude create mode 100644 modules/commitgraph/tests/testgit/logs/HEAD create mode 100644 modules/commitgraph/tests/testgit/logs/refs/heads/master create mode 100644 modules/commitgraph/tests/testgit/objects/43/fb44daf0a8190b40cf385150627c18c164f362 create mode 100644 modules/commitgraph/tests/testgit/objects/5a/a811d3c2f6d5d6e928a4acacd15248928c26d0 create mode 100644 modules/commitgraph/tests/testgit/objects/d7/d0dbbbbd1f40a253ad6287e3d8f0025b27ed86 create mode 100644 modules/commitgraph/tests/testgit/objects/info/commit-graph create mode 100644 modules/commitgraph/tests/testgit/refs/heads/master create mode 100644 modules/git/repo_commitgraph.go diff --git a/models/repo.go b/models/repo.go index 6069be12434db..33cc1b2099e0f 100644 --- a/models/repo.go +++ b/models/repo.go @@ -2240,6 +2240,14 @@ func GitFsck() { func(idx int, bean interface{}) error { repo := bean.(*Repository) repoPath := repo.RepoPath() + // TODO: Move this elsewhere + if gitRepo, err := git.OpenRepository(repoPath); err == nil { + log.Trace("Building commit graph index") + if err := gitRepo.BuildCommitGraph(false); err != nil { + desc := fmt.Sprintf("Failed to build commit graph (%s): %v", repoPath, err) + log.Warn(desc) + } + } log.Trace("Running health check on repository %s", repoPath) if err := git.Fsck(repoPath, setting.Cron.RepoHealthCheck.Timeout, setting.Cron.RepoHealthCheck.Args...); err != nil { desc := fmt.Sprintf("Failed to health check repository (%s): %v", repoPath, err) diff --git a/modules/commitgraph/plumbing/format/commitgraph/bloom.go b/modules/commitgraph/plumbing/format/commitgraph/bloom.go new file mode 100644 index 0000000000000..cfe31ba2c2c1c --- /dev/null +++ b/modules/commitgraph/plumbing/format/commitgraph/bloom.go @@ -0,0 +1,92 @@ +package commitgraph + +import ( + "encoding/binary" + "hash" + "hash/fnv" + + "github.com/dchest/siphash" +) + +type filter struct { + m uint32 + k uint32 + h hash.Hash64 +} + +func (f *filter) bits(data []byte) []uint32 { + f.h.Reset() + f.h.Write(data) + d := f.h.Sum(nil) + a := binary.BigEndian.Uint32(d[4:8]) + b := binary.BigEndian.Uint32(d[0:4]) + is := make([]uint32, f.k) + for i := uint32(0); i < f.k; i++ { + is[i] = (a + b*i) % f.m + } + return is +} + +func newFilter(m, k uint32) *filter { + return &filter{ + m: m, + k: k, + h: fnv.New64(), + } +} + +// BloomPathFilter is a probabilistic data structure that helps determining +// whether a path was was changed. +// +// The implementation uses a standard bloom filter with n=512, m=10, k=7 +// parameters using the 64-bit SipHash hash function with zero key. +type BloomPathFilter struct { + b []byte +} + +// Test checks whether a path was previously added to the filter. Returns +// false if the path is not present in the filter. Returns true if the path +// could be present in the filter. +func (f *BloomPathFilter) Test(path string) bool { + d := siphash.Hash(0, 0, []byte(path)) + a := uint32(d) + b := uint32(d >> 32) + var i uint32 + for i = 0; i < 7; i++ { + bit := (a + b*i) % 5120 + if f.b[bit>>3]&(1<<(bit&7)) == 0 { + return false + } + } + return true +} + +// Add path data to the filter. +func (f *BloomPathFilter) Add(path string) { + d := siphash.Hash(0, 0, []byte(path)) + a := uint32(d) + b := uint32(d >> 32) + var i uint32 + for i = 0; i < 7; i++ { + bit := (a + b*i) % 5120 + f.b[bit>>3] |= 1 << (bit & 7) + } +} + +// Data returns data bytes +func (f *BloomPathFilter) Data() []byte { + return f.b +} + +// NewBloomPathFilter creates a new empty bloom filter +func NewBloomPathFilter() *BloomPathFilter { + f := &BloomPathFilter{make([]byte, 640)} + return f +} + +// LoadBloomPathFilter creates a bloom filter from a byte array previously +// returned by Data +func LoadBloomPathFilter(data []byte) *BloomPathFilter { + f := &BloomPathFilter{data} + return f +} diff --git a/modules/commitgraph/plumbing/format/commitgraph/commitgraph.go b/modules/commitgraph/plumbing/format/commitgraph/commitgraph.go new file mode 100644 index 0000000000000..528362f2f9f85 --- /dev/null +++ b/modules/commitgraph/plumbing/format/commitgraph/commitgraph.go @@ -0,0 +1,38 @@ +package commitgraph + +import ( + "time" + + "gopkg.in/src-d/go-git.v4/plumbing" +) + +// Node is a reduced representation of Commit as presented in the commit graph +// file. It is merely useful as an optimization for walking the commit graphs. +type Node struct { + // TreeHash is the hash of the root tree of the commit. + TreeHash plumbing.Hash + // ParentIndexes are the indexes of the parent commits of the commit. + ParentIndexes []int + // ParentHashes are the hashes of the parent commits of the commit. + ParentHashes []plumbing.Hash + // Generation number is the pre-computed generation in the commit graph + // or zero if not available + Generation int + // When is the timestamp of the commit. + When time.Time +} + +// Index represents a representation of commit graph that allows indexed +// access to the nodes using commit object hash +type Index interface { + // GetIndexByHash gets the index in the commit graph from commit hash, if available + GetIndexByHash(h plumbing.Hash) (int, error) + // GetNodeByIndex gets the commit node from the commit graph using index + // obtained from child node, if available + GetNodeByIndex(i int) (*Node, error) + // Hashes returns all the hashes that are available in the index + Hashes() []plumbing.Hash + + // GetBloomFilterByIndex gets the bloom filter for files changed in the commit, if available + GetBloomFilterByIndex(i int) (*BloomPathFilter, error) +} diff --git a/modules/commitgraph/plumbing/format/commitgraph/commitgraph_test.go b/modules/commitgraph/plumbing/format/commitgraph/commitgraph_test.go new file mode 100644 index 0000000000000..d2b50a9bddde9 --- /dev/null +++ b/modules/commitgraph/plumbing/format/commitgraph/commitgraph_test.go @@ -0,0 +1,35 @@ +package commitgraph_test + +import ( + "testing" + + "code.gitea.io/gitea/modules/commitgraph/plumbing/format/commitgraph" + "golang.org/x/exp/mmap" + + . "gopkg.in/check.v1" + "gopkg.in/src-d/go-git-fixtures.v3" + "gopkg.in/src-d/go-git.v4/plumbing" +) + +func Test(t *testing.T) { TestingT(t) } + +type CommitgraphSuite struct { + fixtures.Suite +} + +var _ = Suite(&CommitgraphSuite{}) + +func (s *CommitgraphSuite) TestDecode(c *C) { + reader, err := mmap.Open("..\\..\\tests\\testgit\\objects\\info\\commit-graph") + c.Assert(err, IsNil) + index, err := commitgraph.OpenFileIndex(reader) + c.Assert(err, IsNil) + + nodeIndex, err := index.GetIndexByHash(plumbing.NewHash("5aa811d3c2f6d5d6e928a4acacd15248928c26d0")) + c.Assert(err, IsNil) + node, err := index.GetNodeByIndex(nodeIndex) + c.Assert(err, IsNil) + c.Assert(len(node.ParentIndexes), Equals, 0) + + reader.Close() +} diff --git a/modules/commitgraph/plumbing/format/commitgraph/encoder.go b/modules/commitgraph/plumbing/format/commitgraph/encoder.go new file mode 100644 index 0000000000000..05d589149b34f --- /dev/null +++ b/modules/commitgraph/plumbing/format/commitgraph/encoder.go @@ -0,0 +1,197 @@ +package commitgraph + +import ( + "bytes" + "crypto/sha1" + "hash" + "io" + "math" + + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/utils/binary" +) + +// Encoder writes MemoryIndex structs to an output stream. +type Encoder struct { + io.Writer + hash hash.Hash +} + +// NewEncoder returns a new stream encoder that writes to w. +func NewEncoder(w io.Writer) *Encoder { + h := sha1.New() + mw := io.MultiWriter(w, h) + return &Encoder{mw, h} +} + +func (e *Encoder) Encode(idx Index) error { + // Get all the hashes in the memory index + hashes := idx.Hashes() + + // Sort the hashes and build our index + plumbing.HashesSort(hashes) + hashToIndex := make(map[plumbing.Hash]uint32) + hashFirstToCount := make(map[byte]uint32) + for i, hash := range hashes { + hashToIndex[hash] = uint32(i) + hashFirstToCount[hash[0]]++ + } + + // Find out if we will need large edge table + chunkCount := 3 + hasLargeEdges := false + for i := 0; i < len(hashes); i++ { + v, _ := idx.GetNodeByIndex(i) + if len(v.ParentHashes) > 2 { + hasLargeEdges = true + chunkCount++ + break + } + } + + // Find out if the bloom filters are present + hasBloomFilters := false + sparseBloomFilters := false + bloomFiltersCount := 0 + for i := 0; i < len(hashes); i++ { + _, err := idx.GetBloomFilterByIndex(i) + if err == nil { + bloomFiltersCount++ + } + } + if bloomFiltersCount > 0 { + hasBloomFilters = true + chunkCount++ + if bloomFiltersCount < (len(hashes) * 4 / 3) { + sparseBloomFilters = true + chunkCount++ + } + } + + var fanoutOffset = uint64(20 + (chunkCount * 12)) + var oidLookupOffset = fanoutOffset + 4*256 + var commitDataOffset = oidLookupOffset + uint64(len(hashes))*20 + var bloomOffset = commitDataOffset + uint64(len(hashes))*36 + var sparseBloomOffset = bloomOffset + uint64(bloomFiltersCount)*640 + var largeEdgeListOffset uint64 + var largeEdges []uint32 + + // Write header + // TODO: Error handling + e.Write(commitFileSignature) + e.Write([]byte{1, 1, byte(chunkCount), 0}) + + // Write chunk headers + e.Write(oidFanoutSignature) + binary.WriteUint64(e, fanoutOffset) + e.Write(oidLookupSignature) + binary.WriteUint64(e, oidLookupOffset) + e.Write(commitDataSignature) + binary.WriteUint64(e, commitDataOffset) + if hasBloomFilters { + e.Write(experimentalBloomSignature) + binary.WriteUint64(e, bloomOffset) + if sparseBloomFilters { + e.Write(experimentalSparseBloomSignature) + binary.WriteUint64(e, sparseBloomOffset) + largeEdgeListOffset = sparseBloomOffset + uint64(len(hashes)+7)/8 + } else { + largeEdgeListOffset = bloomOffset + 640*uint64(len(hashes)) + } + } + if hasLargeEdges { + e.Write(largeEdgeListSignature) + binary.WriteUint64(e, largeEdgeListOffset) + } + e.Write([]byte{0, 0, 0, 0}) + binary.WriteUint64(e, uint64(0)) + + // Write fanout + var cumulative uint32 + for i := 0; i <= 0xff; i++ { + if err := binary.WriteUint32(e, hashFirstToCount[byte(i)]+cumulative); err != nil { + return err + } + cumulative += hashFirstToCount[byte(i)] + } + + // Write OID lookup + for _, hash := range hashes { + if _, err := e.Write(hash[:]); err != nil { + return err + } + } + + // Write commit data + for _, hash := range hashes { + origIndex, _ := idx.GetIndexByHash(hash) + commitData, _ := idx.GetNodeByIndex(origIndex) + if _, err := e.Write(commitData.TreeHash[:]); err != nil { + return err + } + + if len(commitData.ParentHashes) == 0 { + binary.WriteUint32(e, parentNone) + binary.WriteUint32(e, parentNone) + } else if len(commitData.ParentHashes) == 1 { + binary.WriteUint32(e, hashToIndex[commitData.ParentHashes[0]]) + binary.WriteUint32(e, parentNone) + } else if len(commitData.ParentHashes) == 2 { + binary.WriteUint32(e, hashToIndex[commitData.ParentHashes[0]]) + binary.WriteUint32(e, hashToIndex[commitData.ParentHashes[1]]) + } else if len(commitData.ParentHashes) > 2 { + binary.WriteUint32(e, hashToIndex[commitData.ParentHashes[0]]) + binary.WriteUint32(e, uint32(len(largeEdges))|parentOctopusMask) + for _, parentHash := range commitData.ParentHashes[1:] { + largeEdges = append(largeEdges, hashToIndex[parentHash]) + } + largeEdges[len(largeEdges)-1] |= parentLast + } + + unixTime := uint64(commitData.When.Unix()) + unixTime |= uint64(commitData.Generation) << 34 + binary.WriteUint64(e, unixTime) + } + + // Write bloom filters (experimental) + if hasBloomFilters { + var sparseBloomBitset []byte + + if sparseBloomFilters { + sparseBloomBitset = bytes.Repeat([]byte{0xff}, (len(hashes)+7)/8) + } + + for i, hash := range hashes { + origIndex, _ := idx.GetIndexByHash(hash) + if bloomFilter, err := idx.GetBloomFilterByIndex(origIndex); err != nil { + if !sparseBloomFilters { + for i := 0; i < 80; i++ { + binary.WriteUint64(e, math.MaxUint64) + } + } else { + sparseBloomBitset[i/8] &= ^(1 << uint(i%8)) + } + } else { + e.Write(bloomFilter.Data()) + } + } + + if sparseBloomFilters { + e.Write(sparseBloomBitset) + } + } + + // Write large edges if necessary + if hasLargeEdges { + for _, parent := range largeEdges { + binary.WriteUint32(e, parent) + } + } + + // Write checksum + if _, err := e.Write(e.hash.Sum(nil)[:20]); err != nil { + return err + } + + return nil +} diff --git a/modules/commitgraph/plumbing/format/commitgraph/file.go b/modules/commitgraph/plumbing/format/commitgraph/file.go new file mode 100644 index 0000000000000..4156b9ea184ba --- /dev/null +++ b/modules/commitgraph/plumbing/format/commitgraph/file.go @@ -0,0 +1,317 @@ +package commitgraph + +import ( + "bytes" + "errors" + "io" + "math" + "time" + + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/utils/binary" +) + +var ( + // ErrUnsupportedVersion is returned by OpenFileIndex when the commit graph + // file version is not supported. + ErrUnsupportedVersion = errors.New("Unsuported version") + // ErrUnsupportedHash is returned by OpenFileIndex when the commit graph + // hash function is not supported. Currently only SHA-1 is defined and + // supported + ErrUnsupportedHash = errors.New("Unsuported hash algorithm") + // ErrMalformedCommitGraphFile is returned by OpenFileIndex when the commit + // graph file is corrupted. + ErrMalformedCommitGraphFile = errors.New("Malformed commit graph file") + + commitFileSignature = []byte{'C', 'G', 'P', 'H'} + oidFanoutSignature = []byte{'O', 'I', 'D', 'F'} + oidLookupSignature = []byte{'O', 'I', 'D', 'L'} + commitDataSignature = []byte{'C', 'D', 'A', 'T'} + largeEdgeListSignature = []byte{'E', 'D', 'G', 'E'} + experimentalBloomSignature = []byte{'X', 'G', 'G', 'B'} + experimentalSparseBloomSignature = []byte{'X', 'G', 'S', 'B'} + + parentNone = uint32(0x70000000) + parentOctopusUsed = uint32(0x80000000) + parentOctopusMask = uint32(0x7fffffff) + parentLast = uint32(0x80000000) +) + +type fileIndex struct { + reader io.ReaderAt + fanout [256]int + oidLookupOffset int64 + commitDataOffset int64 + largeEdgeListOffset int64 + bloomOffset int64 + sparseBloomOffset int64 + sparseBloomMap map[int]int +} + +// OpenFileIndex opens a serialized commit graph file in the format described at +// https://github.com/git/git/blob/master/Documentation/technical/commit-graph-format.txt +func OpenFileIndex(reader io.ReaderAt) (Index, error) { + // Verify file signature + var signature = make([]byte, 4) + if _, err := reader.ReadAt(signature, 0); err != nil { + return nil, err + } + if !bytes.Equal(signature, commitFileSignature) { + return nil, ErrMalformedCommitGraphFile + } + + // Read and verify the file header + var header = make([]byte, 4) + if _, err := reader.ReadAt(header, 4); err != nil { + return nil, err + } + if header[0] != 1 { + return nil, ErrUnsupportedVersion + } + if header[1] != 1 { + return nil, ErrUnsupportedHash + } + + // Read chunk headers + var chunkID = make([]byte, 4) + var oidFanoutOffset int64 + var oidLookupOffset int64 + var commitDataOffset int64 + var largeEdgeListOffset int64 + var bloomOffset int64 + var sparseBloomOffset int64 + chunkCount := int(header[2]) + for i := 0; i < chunkCount; i++ { + chunkHeader := io.NewSectionReader(reader, 8+(int64(i)*12), 12) + if _, err := io.ReadAtLeast(chunkHeader, chunkID, 4); err != nil { + return nil, err + } + chunkOffset, err := binary.ReadUint64(chunkHeader) + if err != nil { + return nil, err + } + + if bytes.Equal(chunkID, oidFanoutSignature) { + oidFanoutOffset = int64(chunkOffset) + } else if bytes.Equal(chunkID, oidLookupSignature) { + oidLookupOffset = int64(chunkOffset) + } else if bytes.Equal(chunkID, commitDataSignature) { + commitDataOffset = int64(chunkOffset) + } else if bytes.Equal(chunkID, largeEdgeListSignature) { + largeEdgeListOffset = int64(chunkOffset) + } else if bytes.Equal(chunkID, experimentalBloomSignature) { + bloomOffset = int64(chunkOffset) + } else if bytes.Equal(chunkID, experimentalSparseBloomSignature) { + sparseBloomOffset = int64(chunkOffset) + } + } + + if oidFanoutOffset <= 0 || oidLookupOffset <= 0 || commitDataOffset <= 0 { + return nil, ErrMalformedCommitGraphFile + } + + // Read fanout table and calculate the file offsets into the lookup table + fanoutReader := io.NewSectionReader(reader, oidFanoutOffset, 256*4) + var fanout [256]int + for i := 0; i < 256; i++ { + fanoutValue, err := binary.ReadUint32(fanoutReader) + if err != nil { + return nil, err + } + if fanoutValue > 0x7fffffff { + return nil, ErrMalformedCommitGraphFile + } + fanout[i] = int(fanoutValue) + } + + return &fileIndex{reader, fanout, oidLookupOffset, commitDataOffset, largeEdgeListOffset, bloomOffset, sparseBloomOffset, nil}, nil +} + +func (fi *fileIndex) GetIndexByHash(h plumbing.Hash) (int, error) { + var oid plumbing.Hash + + // Find the hash in the oid lookup table + var low int + if h[0] == 0 { + low = 0 + } else { + low = fi.fanout[h[0]-1] + } + high := fi.fanout[h[0]] + for low < high { + mid := (low + high) >> 1 + offset := fi.oidLookupOffset + int64(mid)*20 + if _, err := fi.reader.ReadAt(oid[:], offset); err != nil { + return 0, err + } + cmp := bytes.Compare(h[:], oid[:]) + if cmp < 0 { + high = mid + } else if cmp == 0 { + return mid, nil + } else { + low = mid + 1 + } + } + + return 0, plumbing.ErrObjectNotFound +} + +func (fi *fileIndex) GetNodeByIndex(idx int) (*Node, error) { + if idx >= fi.fanout[0xff] { + return nil, plumbing.ErrObjectNotFound + } + + offset := fi.commitDataOffset + int64(idx)*36 + commitDataReader := io.NewSectionReader(fi.reader, offset, 36) + + treeHash, err := binary.ReadHash(commitDataReader) + if err != nil { + return nil, err + } + parent1, err := binary.ReadUint32(commitDataReader) + if err != nil { + return nil, err + } + parent2, err := binary.ReadUint32(commitDataReader) + if err != nil { + return nil, err + } + genAndTime, err := binary.ReadUint64(commitDataReader) + if err != nil { + return nil, err + } + + var parentIndexes []int + if parent2&parentOctopusUsed == parentOctopusUsed { + // Octopus merge + parentIndexes = []int{int(parent1 & parentOctopusMask)} + offset := fi.largeEdgeListOffset + 4*int64(parent2&parentOctopusMask) + parentReader := io.NewSectionReader(fi.reader, offset, math.MaxInt64) + for { + parent, err := binary.ReadUint32(parentReader) + if err != nil { + return nil, err + } + parentIndexes = append(parentIndexes, int(parent&parentOctopusMask)) + if parent&parentLast == parentLast { + break + } + } + } else if parent2 != parentNone { + parentIndexes = []int{int(parent1 & parentOctopusMask), int(parent2 & parentOctopusMask)} + } else if parent1 != parentNone { + parentIndexes = []int{int(parent1 & parentOctopusMask)} + } + + parentHashes, err := fi.getHashesFromIndexes(parentIndexes) + if err != nil { + return nil, err + } + + return &Node{ + TreeHash: treeHash, + ParentIndexes: parentIndexes, + ParentHashes: parentHashes, + Generation: int(genAndTime >> 34), + When: time.Unix(int64(genAndTime&0x3FFFFFFFF), 0), + }, nil +} + +func (fi *fileIndex) getHashesFromIndexes(indexes []int) ([]plumbing.Hash, error) { + hashes := make([]plumbing.Hash, len(indexes)) + + for i, idx := range indexes { + if idx > fi.fanout[0xff] { + return nil, ErrMalformedCommitGraphFile + } + + offset := fi.oidLookupOffset + int64(idx)*20 + if _, err := fi.reader.ReadAt(hashes[i][:], offset); err != nil { + return nil, err + } + } + + return hashes, nil +} + +// Hashes returns all the hashes that are available in the index +func (fi *fileIndex) Hashes() []plumbing.Hash { + hashes := make([]plumbing.Hash, 0, fi.fanout[0xff]) + for i := 0; i < int(fi.fanout[0xff]); i++ { + offset := fi.oidLookupOffset + int64(i)*20 + if _, err := fi.reader.ReadAt(hashes[i][:], offset); err != nil { + return nil + } + } + return hashes +} + +// GetBloomFilterByIndex gets the bloom filter for files changed in the commit, if available +func (fi *fileIndex) GetBloomFilterByIndex(i int) (*BloomPathFilter, error) { + if fi.bloomOffset == 0 || i >= fi.fanout[0xff] { + return nil, plumbing.ErrObjectNotFound + } + + if fi.sparseBloomOffset != 0 { + if fi.sparseBloomMap != nil { + // Build the map + fi.sparseBloomMap = make(map[int]int) + sparseBloomSize := (fi.fanout[0xff] + 7) / 8 + sparseBloomReader := io.NewSectionReader(fi.reader, fi.sparseBloomOffset, int64(sparseBloomSize)) + sparseBloomBits := make([]byte, sparseBloomSize) + if _, err := io.ReadAtLeast(sparseBloomReader, sparseBloomBits, sparseBloomSize); err != nil { + return nil, err + } + + for idx, sidx := 0, 0; idx < sparseBloomSize; idx++ { + if sparseBloomBits[idx]&1 > 0 { + fi.sparseBloomMap[idx*8] = sidx + sidx++ + } + if sparseBloomBits[idx]&2 > 0 { + fi.sparseBloomMap[idx*8+1] = sidx + sidx++ + } + if sparseBloomBits[idx]&4 > 0 { + fi.sparseBloomMap[idx*8+2] = sidx + sidx++ + } + if sparseBloomBits[idx]&8 > 0 { + fi.sparseBloomMap[idx*8+3] = sidx + sidx++ + } + if sparseBloomBits[idx]&16 > 0 { + fi.sparseBloomMap[idx*8+4] = sidx + sidx++ + } + if sparseBloomBits[idx]&32 > 0 { + fi.sparseBloomMap[idx*8+5] = sidx + sidx++ + } + if sparseBloomBits[idx]&64 > 0 { + fi.sparseBloomMap[idx*8+6] = sidx + sidx++ + } + if sparseBloomBits[idx]&128 > 0 { + fi.sparseBloomMap[idx*8+7] = sidx + sidx++ + } + } + } + + // Map the original index into the sparse one + var ok bool + if i, ok = fi.sparseBloomMap[i]; !ok { + return nil, plumbing.ErrObjectNotFound + } + } + + offset := fi.bloomOffset + int64(i)*640 + bloomReader := io.NewSectionReader(fi.reader, offset, 640) + bloomBits := make([]byte, 640) + if _, err := io.ReadAtLeast(bloomReader, bloomBits, 640); err != nil { + return nil, err + } + return LoadBloomPathFilter(bloomBits), nil +} diff --git a/modules/commitgraph/plumbing/format/commitgraph/memory.go b/modules/commitgraph/plumbing/format/commitgraph/memory.go new file mode 100644 index 0000000000000..6dfd0b0af08be --- /dev/null +++ b/modules/commitgraph/plumbing/format/commitgraph/memory.go @@ -0,0 +1,78 @@ +package commitgraph + +import ( + "gopkg.in/src-d/go-git.v4/plumbing" +) + +type MemoryIndex struct { + commitData []*Node + bloomFilters []*BloomPathFilter + indexMap map[plumbing.Hash]int +} + +// NewMemoryIndex creates in-memory commit graph representation +func NewMemoryIndex() *MemoryIndex { + return &MemoryIndex{ + indexMap: make(map[plumbing.Hash]int), + } +} + +// GetIndexByHash gets the index in the commit graph from commit hash, if available +func (mi *MemoryIndex) GetIndexByHash(h plumbing.Hash) (int, error) { + i, ok := mi.indexMap[h] + if ok { + return i, nil + } + + return 0, plumbing.ErrObjectNotFound +} + +// GetNodeByIndex gets the commit node from the commit graph using index +// obtained from child node, if available +func (mi *MemoryIndex) GetNodeByIndex(i int) (*Node, error) { + if int(i) >= len(mi.commitData) { + return nil, plumbing.ErrObjectNotFound + } + + return mi.commitData[i], nil +} + +// Hashes returns all the hashes that are available in the index +func (mi *MemoryIndex) Hashes() []plumbing.Hash { + hashes := make([]plumbing.Hash, 0, len(mi.indexMap)) + for k := range mi.indexMap { + hashes = append(hashes, k) + } + return hashes +} + +// GetBloomFilterByIndex gets the bloom filter for files changed in the commit, if available +func (mi *MemoryIndex) GetBloomFilterByIndex(i int) (*BloomPathFilter, error) { + if int(i) >= len(mi.bloomFilters) || mi.bloomFilters[i] == nil { + return nil, plumbing.ErrObjectNotFound + } + + return mi.bloomFilters[i], nil +} + +// Add adds new node to the memory index +func (mi *MemoryIndex) Add(hash plumbing.Hash, node *Node) error { + return mi.AddWithBloom(hash, node, nil) +} + +// AddWithBloom adds new node to the memory index, including the optional bloom filter for changed files +func (mi *MemoryIndex) AddWithBloom(hash plumbing.Hash, node *Node, bloom *BloomPathFilter) error { + // Map parent hashes to parent indexes + parentIndexes := make([]int, len(node.ParentHashes)) + for i, parentHash := range node.ParentHashes { + var err error + if parentIndexes[i], err = mi.GetIndexByHash(parentHash); err != nil { + return err + } + } + node.ParentIndexes = parentIndexes + mi.indexMap[hash] = len(mi.commitData) + mi.commitData = append(mi.commitData, node) + mi.bloomFilters = append(mi.bloomFilters, bloom) + return nil +} diff --git a/modules/commitgraph/plumbing/object/commitnode.go b/modules/commitgraph/plumbing/object/commitnode.go new file mode 100644 index 0000000000000..5bd92364c32c2 --- /dev/null +++ b/modules/commitgraph/plumbing/object/commitnode.go @@ -0,0 +1,328 @@ +package object + +import ( + "fmt" + "io" + "time" + + "code.gitea.io/gitea/modules/commitgraph/plumbing/format/commitgraph" + + "gopkg.in/src-d/go-git.v4/plumbing" + ggobject "gopkg.in/src-d/go-git.v4/plumbing/object" + "gopkg.in/src-d/go-git.v4/plumbing/storer" +) + +// CommitNode is generic interface encapsulating either Commit object or +// graphCommitNode object +type CommitNode interface { + ID() plumbing.Hash + Tree() (*ggobject.Tree, error) + CommitTime() time.Time +} + +// CommitNodeIndex is generic interface encapsulating an index of CommitNode objects +// and accessor methods for walking it as a directed graph +type CommitNodeIndex interface { + NumParents(node CommitNode) int + ParentNodes(node CommitNode) CommitNodeIter + ParentNode(node CommitNode, i int) (CommitNode, error) + ParentHashes(node CommitNode) []plumbing.Hash + + NodeFromHash(hash plumbing.Hash) (CommitNode, error) + + // Commit returns the full commit object from the node + Commit(node CommitNode) (*ggobject.Commit, error) + + // BloomFilter returns optional bloom filter for changed file paths + BloomFilter(node CommitNode) (*commitgraph.BloomPathFilter, error) +} + +// CommitNodeIter is a generic closable interface for iterating over commit nodes. +type CommitNodeIter interface { + Next() (CommitNode, error) + ForEach(func(CommitNode) error) error + Close() +} + +// graphCommitNode is a reduced representation of Commit as presented in the commit +// graph file (commitgraph.Node). It is merely useful as an optimization for walking +// the commit graphs. +// +// graphCommitNode implements the CommitNode interface. +type graphCommitNode struct { + // Hash for the Commit object + hash plumbing.Hash + // Index of the node in the commit graph file + index int + + node *commitgraph.Node + gci *graphCommitNodeIndex +} + +// graphCommitNodeIndex is an index that can load CommitNode objects from both the commit +// graph files and the object store. +// +// graphCommitNodeIndex implements the CommitNodeIndex interface +type graphCommitNodeIndex struct { + commitGraph commitgraph.Index + s storer.EncodedObjectStorer +} + +// objectCommitNode is a representation of Commit as presented in the GIT object format. +// +// objectCommitNode implements the CommitNode interface. +type objectCommitNode struct { + commit *ggobject.Commit +} + +// objectCommitNodeIndex is an index that can load CommitNode objects only from the +// object store. +// +// objectCommitNodeIndex implements the CommitNodeIndex interface +type objectCommitNodeIndex struct { + s storer.EncodedObjectStorer +} + +// ID returns the Commit object id referenced by the commit graph node. +func (c *graphCommitNode) ID() plumbing.Hash { + return c.hash +} + +// Tree returns the Tree referenced by the commit graph node. +func (c *graphCommitNode) Tree() (*ggobject.Tree, error) { + return ggobject.GetTree(c.gci.s, c.node.TreeHash) +} + +// CommitTime returns the Commiter.When time of the Commit referenced by the commit graph node. +func (c *graphCommitNode) CommitTime() time.Time { + return c.node.When +} + +func (c *graphCommitNode) String() string { + return fmt.Sprintf( + "%s %s\nDate: %s", + plumbing.CommitObject, c.ID(), + c.CommitTime().Format(ggobject.DateFormat), + ) +} + +func NewGraphCommitNodeIndex(commitGraph commitgraph.Index, s storer.EncodedObjectStorer) CommitNodeIndex { + return &graphCommitNodeIndex{commitGraph, s} +} + +// NumParents returns the number of parents in a commit. +func (gci *graphCommitNodeIndex) NumParents(node CommitNode) int { + if cgn, ok := node.(*graphCommitNode); ok { + return len(cgn.node.ParentIndexes) + } + co := node.(*objectCommitNode) + return co.commit.NumParents() +} + +// ParentNodes return a CommitNodeIter for parents of specified node. +func (gci *graphCommitNodeIndex) ParentNodes(node CommitNode) CommitNodeIter { + return newParentgraphCommitNodeIter(gci, node) +} + +// ParentNode returns the ith parent of a commit. +func (gci *graphCommitNodeIndex) ParentNode(node CommitNode, i int) (CommitNode, error) { + if cgn, ok := node.(*graphCommitNode); ok { + if len(cgn.node.ParentIndexes) == 0 || i >= len(cgn.node.ParentIndexes) { + return nil, ggobject.ErrParentNotFound + } + + parent, err := gci.commitGraph.GetNodeByIndex(cgn.node.ParentIndexes[i]) + if err != nil { + return nil, err + } + + return &graphCommitNode{ + hash: cgn.node.ParentHashes[i], + index: cgn.node.ParentIndexes[i], + node: parent, + gci: gci, + }, nil + } + + co := node.(*objectCommitNode) + if len(co.commit.ParentHashes) == 0 || i >= len(co.commit.ParentHashes) { + return nil, ggobject.ErrParentNotFound + } + + parentHash := co.commit.ParentHashes[i] + return gci.NodeFromHash(parentHash) +} + +// ParentHashes returns hashes of the parent commits for a specified node +func (gci *graphCommitNodeIndex) ParentHashes(node CommitNode) []plumbing.Hash { + if cgn, ok := node.(*graphCommitNode); ok { + return cgn.node.ParentHashes + } + co := node.(*objectCommitNode) + return co.commit.ParentHashes +} + +// NodeFromHash looks up a commit node by it's object hash +func (gci *graphCommitNodeIndex) NodeFromHash(hash plumbing.Hash) (CommitNode, error) { + // Check the commit graph first + parentIndex, err := gci.commitGraph.GetIndexByHash(hash) + if err == nil { + parent, err := gci.commitGraph.GetNodeByIndex(parentIndex) + if err != nil { + return nil, err + } + + return &graphCommitNode{ + hash: hash, + index: parentIndex, + node: parent, + gci: gci, + }, nil + } + + // Fallback to loading full commit object + commit, err := ggobject.GetCommit(gci.s, hash) + if err != nil { + return nil, err + } + + return &objectCommitNode{commit: commit}, nil +} + +// Commit returns the full Commit object representing the commit graph node. +func (gci *graphCommitNodeIndex) Commit(node CommitNode) (*ggobject.Commit, error) { + if cgn, ok := node.(*graphCommitNode); ok { + return ggobject.GetCommit(gci.s, cgn.ID()) + } + co := node.(*objectCommitNode) + return co.commit, nil +} + +// BloomFilter returns optional bloom filter for changed file paths +func (gci *graphCommitNodeIndex) BloomFilter(node CommitNode) (*commitgraph.BloomPathFilter, error) { + if cgn, ok := node.(*graphCommitNode); ok { + return gci.commitGraph.GetBloomFilterByIndex(cgn.index) + } + return nil, plumbing.ErrObjectNotFound +} + +// CommitTime returns the time when the commit was performed. +// +// CommitTime is present to fulfill the CommitNode interface. +func (c *objectCommitNode) CommitTime() time.Time { + return c.commit.Committer.When +} + +// ID returns the Commit object id referenced by the node. +func (c *objectCommitNode) ID() plumbing.Hash { + return c.commit.ID() +} + +// Tree returns the Tree referenced by the node. +func (c *objectCommitNode) Tree() (*ggobject.Tree, error) { + return c.commit.Tree() +} + +func NewObjectCommitNodeIndex(s storer.EncodedObjectStorer) CommitNodeIndex { + return &objectCommitNodeIndex{s} +} + +// NumParents returns the number of parents in a commit. +func (oci *objectCommitNodeIndex) NumParents(node CommitNode) int { + co := node.(*objectCommitNode) + return co.commit.NumParents() +} + +// ParentNodes return a CommitNodeIter for parents of specified node. +func (oci *objectCommitNodeIndex) ParentNodes(node CommitNode) CommitNodeIter { + return newParentgraphCommitNodeIter(oci, node) +} + +// ParentNode returns the ith parent of a commit. +func (oci *objectCommitNodeIndex) ParentNode(node CommitNode, i int) (CommitNode, error) { + co := node.(*objectCommitNode) + parent, err := co.commit.Parent(i) + if err != nil { + return nil, err + } + return &objectCommitNode{commit: parent}, nil +} + +// ParentHashes returns hashes of the parent commits for a specified node +func (oci *objectCommitNodeIndex) ParentHashes(node CommitNode) []plumbing.Hash { + co := node.(*objectCommitNode) + return co.commit.ParentHashes +} + +// NodeFromHash looks up a commit node by it's object hash +func (oci *objectCommitNodeIndex) NodeFromHash(hash plumbing.Hash) (CommitNode, error) { + commit, err := ggobject.GetCommit(oci.s, hash) + if err != nil { + return nil, err + } + + return &objectCommitNode{commit: commit}, nil +} + +// Commit returns the full Commit object representing the commit graph node. +func (oci *objectCommitNodeIndex) Commit(node CommitNode) (*ggobject.Commit, error) { + co := node.(*objectCommitNode) + return co.commit, nil +} + +// BloomFilter returns optional bloom filter for changed file paths +func (oci *objectCommitNodeIndex) BloomFilter(node CommitNode) (*commitgraph.BloomPathFilter, error) { + return nil, plumbing.ErrObjectNotFound +} + +// parentCommitNodeIter provides an iterator for parent commits from associated CommitNodeIndex. +type parentCommitNodeIter struct { + gci CommitNodeIndex + node CommitNode + i int +} + +func newParentgraphCommitNodeIter(gci CommitNodeIndex, node CommitNode) CommitNodeIter { + return &parentCommitNodeIter{gci, node, 0} +} + +// Next moves the iterator to the next commit and returns a pointer to it. If +// there are no more commits, it returns io.EOF. +func (iter *parentCommitNodeIter) Next() (CommitNode, error) { + obj, err := iter.gci.ParentNode(iter.node, iter.i) + if err == ggobject.ErrParentNotFound { + return nil, io.EOF + } + if err == nil { + iter.i++ + } + + return obj, err +} + +// ForEach call the cb function for each commit contained on this iter until +// an error appends or the end of the iter is reached. If ErrStop is sent +// the iteration is stopped but no error is returned. The iterator is closed. +func (iter *parentCommitNodeIter) ForEach(cb func(CommitNode) error) error { + for { + obj, err := iter.Next() + if err != nil { + if err == io.EOF { + return nil + } + + return err + } + + if err := cb(obj); err != nil { + if err == storer.ErrStop { + return nil + } + + return err + } + } +} + +func (iter *parentCommitNodeIter) Close() { +} diff --git a/modules/commitgraph/plumbing/object/commitnode_walker_ctime.go b/modules/commitgraph/plumbing/object/commitnode_walker_ctime.go new file mode 100644 index 0000000000000..86b6c5765c493 --- /dev/null +++ b/modules/commitgraph/plumbing/object/commitnode_walker_ctime.go @@ -0,0 +1,108 @@ +package object + +import ( + "io" + + "github.com/emirpasic/gods/trees/binaryheap" + + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/storer" +) + +type commitNodeIteratorByCTime struct { + heap *binaryheap.Heap + seenExternal map[plumbing.Hash]bool + seen map[plumbing.Hash]bool + nodeIndex CommitNodeIndex +} + +// NewCommitNodeIterCTime returns a CommitNodeIter that walks the commit history, +// starting at the given commit and visiting its parents while preserving Committer Time order. +// this appears to be the closest order to `git log` +// The given callback will be called for each visited commit. Each commit will +// be visited only once. If the callback returns an error, walking will stop +// and will return the error. Other errors might be returned if the history +// cannot be traversed (e.g. missing objects). Ignore allows to skip some +// commits from being iterated. +func NewCommitNodeIterCTime( + c CommitNode, + nodeIndex CommitNodeIndex, + seenExternal map[plumbing.Hash]bool, + ignore []plumbing.Hash, +) CommitNodeIter { + seen := make(map[plumbing.Hash]bool) + for _, h := range ignore { + seen[h] = true + } + + heap := binaryheap.NewWith(func(a, b interface{}) int { + if a.(CommitNode).CommitTime().Before(b.(CommitNode).CommitTime()) { + return 1 + } + return -1 + }) + + heap.Push(c) + + return &commitNodeIteratorByCTime{ + heap: heap, + seenExternal: seenExternal, + seen: seen, + nodeIndex: nodeIndex, + } +} + +func (w *commitNodeIteratorByCTime) Next() (CommitNode, error) { + var c CommitNode + for { + cIn, ok := w.heap.Pop() + if !ok { + return nil, io.EOF + } + c = cIn.(CommitNode) + cID := c.ID() + + if w.seen[cID] || w.seenExternal[cID] { + continue + } + + w.seen[cID] = true + + for i, h := range w.nodeIndex.ParentHashes(c) { + if w.seen[h] || w.seenExternal[h] { + continue + } + pc, err := w.nodeIndex.ParentNode(c, i) + if err != nil { + return nil, err + } + w.heap.Push(pc) + } + + return c, nil + } +} + +func (w *commitNodeIteratorByCTime) ForEach(cb func(CommitNode) error) error { + for { + c, err := w.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + + err = cb(c) + if err == storer.ErrStop { + break + } + if err != nil { + return err + } + } + + return nil +} + +func (w *commitNodeIteratorByCTime) Close() {} diff --git a/modules/commitgraph/tests/testgit/COMMIT_EDITMSG b/modules/commitgraph/tests/testgit/COMMIT_EDITMSG new file mode 100644 index 0000000000000..022d2a7fa1de9 --- /dev/null +++ b/modules/commitgraph/tests/testgit/COMMIT_EDITMSG @@ -0,0 +1 @@ +Boo diff --git a/modules/commitgraph/tests/testgit/FETCH_HEAD b/modules/commitgraph/tests/testgit/FETCH_HEAD new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/modules/commitgraph/tests/testgit/HEAD b/modules/commitgraph/tests/testgit/HEAD new file mode 100644 index 0000000000000..cb089cd89a7d7 --- /dev/null +++ b/modules/commitgraph/tests/testgit/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/modules/commitgraph/tests/testgit/config b/modules/commitgraph/tests/testgit/config new file mode 100644 index 0000000000000..b25815843ac47 --- /dev/null +++ b/modules/commitgraph/tests/testgit/config @@ -0,0 +1,9 @@ +[core] + bare = false + repositoryformatversion = 0 + filemode = false + symlinks = false + ignorecase = true + logallrefupdates = true +[remote "origin"] + puttykeyfile = diff --git a/modules/commitgraph/tests/testgit/description b/modules/commitgraph/tests/testgit/description new file mode 100644 index 0000000000000..498b267a8c781 --- /dev/null +++ b/modules/commitgraph/tests/testgit/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/modules/commitgraph/tests/testgit/hooks/applypatch-msg.sample b/modules/commitgraph/tests/testgit/hooks/applypatch-msg.sample new file mode 100644 index 0000000000000..a5d7b84a67345 --- /dev/null +++ b/modules/commitgraph/tests/testgit/hooks/applypatch-msg.sample @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, rename this file to "applypatch-msg". + +. git-sh-setup +commitmsg="$(git rev-parse --git-path hooks/commit-msg)" +test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} +: diff --git a/modules/commitgraph/tests/testgit/hooks/commit-msg.sample b/modules/commitgraph/tests/testgit/hooks/commit-msg.sample new file mode 100644 index 0000000000000..b58d1184a9d43 --- /dev/null +++ b/modules/commitgraph/tests/testgit/hooks/commit-msg.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, rename this file to "commit-msg". + +# Uncomment the below to add a Signed-off-by line to the message. +# Doing this in a hook is a bad idea in general, but the prepare-commit-msg +# hook is more suited to it. +# +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} diff --git a/modules/commitgraph/tests/testgit/hooks/fsmonitor-watchman.sample b/modules/commitgraph/tests/testgit/hooks/fsmonitor-watchman.sample new file mode 100644 index 0000000000000..e673bb3980f3c --- /dev/null +++ b/modules/commitgraph/tests/testgit/hooks/fsmonitor-watchman.sample @@ -0,0 +1,114 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use IPC::Open2; + +# An example hook script to integrate Watchman +# (https://facebook.github.io/watchman/) with git to speed up detecting +# new and modified files. +# +# The hook is passed a version (currently 1) and a time in nanoseconds +# formatted as a string and outputs to stdout all files that have been +# modified since the given time. Paths must be relative to the root of +# the working tree and separated by a single NUL. +# +# To enable this hook, rename this file to "query-watchman" and set +# 'git config core.fsmonitor .git/hooks/query-watchman' +# +my ($version, $time) = @ARGV; + +# Check the hook interface version + +if ($version == 1) { + # convert nanoseconds to seconds + $time = int $time / 1000000000; +} else { + die "Unsupported query-fsmonitor hook version '$version'.\n" . + "Falling back to scanning...\n"; +} + +my $git_work_tree; +if ($^O =~ 'msys' || $^O =~ 'cygwin') { + $git_work_tree = Win32::GetCwd(); + $git_work_tree =~ tr/\\/\//; +} else { + require Cwd; + $git_work_tree = Cwd::cwd(); +} + +my $retry = 1; + +launch_watchman(); + +sub launch_watchman { + + my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') + or die "open2() failed: $!\n" . + "Falling back to scanning...\n"; + + # In the query expression below we're asking for names of files that + # changed since $time but were not transient (ie created after + # $time but no longer exist). + # + # To accomplish this, we're using the "since" generator to use the + # recency index to select candidate nodes and "fields" to limit the + # output to file names only. Then we're using the "expression" term to + # further constrain the results. + # + # The category of transient files that we want to ignore will have a + # creation clock (cclock) newer than $time_t value and will also not + # currently exist. + + my $query = <<" END"; + ["query", "$git_work_tree", { + "since": $time, + "fields": ["name"], + "expression": ["not", ["allof", ["since", $time, "cclock"], ["not", "exists"]]] + }] + END + + print CHLD_IN $query; + close CHLD_IN; + my $response = do {local $/; }; + + die "Watchman: command returned no output.\n" . + "Falling back to scanning...\n" if $response eq ""; + die "Watchman: command returned invalid output: $response\n" . + "Falling back to scanning...\n" unless $response =~ /^\{/; + + my $json_pkg; + eval { + require JSON::XS; + $json_pkg = "JSON::XS"; + 1; + } or do { + require JSON::PP; + $json_pkg = "JSON::PP"; + }; + + my $o = $json_pkg->new->utf8->decode($response); + + if ($retry > 0 and $o->{error} and $o->{error} =~ m/unable to resolve root .* directory (.*) is not watched/) { + print STDERR "Adding '$git_work_tree' to watchman's watch list.\n"; + $retry--; + qx/watchman watch "$git_work_tree"/; + die "Failed to make watchman watch '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + + # Watchman will always return all files on the first query so + # return the fast "everything is dirty" flag to git and do the + # Watchman query just to get it over with now so we won't pay + # the cost in git to look up each individual file. + print "/\0"; + eval { launch_watchman() }; + exit 0; + } + + die "Watchman: $o->{error}.\n" . + "Falling back to scanning...\n" if $o->{error}; + + binmode STDOUT, ":utf8"; + local $, = "\0"; + print @{$o->{files}}; +} diff --git a/modules/commitgraph/tests/testgit/hooks/post-update.sample b/modules/commitgraph/tests/testgit/hooks/post-update.sample new file mode 100644 index 0000000000000..ec17ec1939b7c --- /dev/null +++ b/modules/commitgraph/tests/testgit/hooks/post-update.sample @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, rename this file to "post-update". + +exec git update-server-info diff --git a/modules/commitgraph/tests/testgit/hooks/pre-applypatch.sample b/modules/commitgraph/tests/testgit/hooks/pre-applypatch.sample new file mode 100644 index 0000000000000..4142082bcb939 --- /dev/null +++ b/modules/commitgraph/tests/testgit/hooks/pre-applypatch.sample @@ -0,0 +1,14 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-applypatch". + +. git-sh-setup +precommit="$(git rev-parse --git-path hooks/pre-commit)" +test -x "$precommit" && exec "$precommit" ${1+"$@"} +: diff --git a/modules/commitgraph/tests/testgit/hooks/pre-commit.sample b/modules/commitgraph/tests/testgit/hooks/pre-commit.sample new file mode 100644 index 0000000000000..6a756416384c2 --- /dev/null +++ b/modules/commitgraph/tests/testgit/hooks/pre-commit.sample @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=$(git hash-object -t tree /dev/null) +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- diff --git a/modules/commitgraph/tests/testgit/hooks/pre-push.sample b/modules/commitgraph/tests/testgit/hooks/pre-push.sample new file mode 100644 index 0000000000000..6187dbf4390fc --- /dev/null +++ b/modules/commitgraph/tests/testgit/hooks/pre-push.sample @@ -0,0 +1,53 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +z40=0000000000000000000000000000000000000000 + +while read local_ref local_sha remote_ref remote_sha +do + if [ "$local_sha" = $z40 ] + then + # Handle delete + : + else + if [ "$remote_sha" = $z40 ] + then + # New branch, examine all commits + range="$local_sha" + else + # Update to existing branch, examine new commits + range="$remote_sha..$local_sha" + fi + + # Check for WIP commit + commit=`git rev-list -n 1 --grep '^WIP' "$range"` + if [ -n "$commit" ] + then + echo >&2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/modules/commitgraph/tests/testgit/hooks/pre-rebase.sample b/modules/commitgraph/tests/testgit/hooks/pre-rebase.sample new file mode 100644 index 0000000000000..6cbef5c370d8c --- /dev/null +++ b/modules/commitgraph/tests/testgit/hooks/pre-rebase.sample @@ -0,0 +1,169 @@ +#!/bin/sh +# +# Copyright (c) 2006, 2008 Junio C Hamano +# +# The "pre-rebase" hook is run just before "git rebase" starts doing +# its job, and can prevent the command from running by exiting with +# non-zero status. +# +# The hook is called with the following parameters: +# +# $1 -- the upstream the series was forked from. +# $2 -- the branch being rebased (or empty when rebasing the current branch). +# +# This sample shows how to prevent topic branches that are already +# merged to 'next' branch from getting rebased, because allowing it +# would result in rebasing already published history. + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` || + exit 0 ;# we do not interrupt rebasing detached HEAD +fi + +case "$topic" in +refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Does the topic really exist? +git show-ref -q "$topic" || { + echo >&2 "No such branch $topic" + exit 1 +} + +# Is topic fully merged to master? +not_in_master=`git rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up to date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` + /usr/bin/perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +<<\DOC_END + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git rev-list ^master ^topic next + git rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git rev-list master..topic + + if this is empty, it is fully merged to "master". + +DOC_END diff --git a/modules/commitgraph/tests/testgit/hooks/pre-receive.sample b/modules/commitgraph/tests/testgit/hooks/pre-receive.sample new file mode 100644 index 0000000000000..a1fd29ec14823 --- /dev/null +++ b/modules/commitgraph/tests/testgit/hooks/pre-receive.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to make use of push options. +# The example simply echoes all push options that start with 'echoback=' +# and rejects all pushes when the "reject" push option is used. +# +# To enable this hook, rename this file to "pre-receive". + +if test -n "$GIT_PUSH_OPTION_COUNT" +then + i=0 + while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" + do + eval "value=\$GIT_PUSH_OPTION_$i" + case "$value" in + echoback=*) + echo "echo from the pre-receive-hook: ${value#*=}" >&2 + ;; + reject) + exit 1 + esac + i=$((i + 1)) + done +fi diff --git a/modules/commitgraph/tests/testgit/hooks/prepare-commit-msg.sample b/modules/commitgraph/tests/testgit/hooks/prepare-commit-msg.sample new file mode 100644 index 0000000000000..10fa14c5ab013 --- /dev/null +++ b/modules/commitgraph/tests/testgit/hooks/prepare-commit-msg.sample @@ -0,0 +1,42 @@ +#!/bin/sh +# +# An example hook script to prepare the commit log message. +# Called by "git commit" with the name of the file that has the +# commit message, followed by the description of the commit +# message's source. The hook's purpose is to edit the commit +# message file. If the hook fails with a non-zero status, +# the commit is aborted. +# +# To enable this hook, rename this file to "prepare-commit-msg". + +# This hook includes three examples. The first one removes the +# "# Please enter the commit message..." help message. +# +# The second includes the output of "git diff --name-status -r" +# into the message, just before the "git status" output. It is +# commented because it doesn't cope with --amend or with squashed +# commits. +# +# The third example adds a Signed-off-by line to the message, that can +# still be edited. This is rarely a good idea. + +COMMIT_MSG_FILE=$1 +COMMIT_SOURCE=$2 +SHA1=$3 + +/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" + +# case "$COMMIT_SOURCE,$SHA1" in +# ,|template,) +# /usr/bin/perl -i.bak -pe ' +# print "\n" . `git diff --cached --name-status -r` +# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; +# *) ;; +# esac + +# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE" +# if test -z "$COMMIT_SOURCE" +# then +# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE" +# fi diff --git a/modules/commitgraph/tests/testgit/hooks/update.sample b/modules/commitgraph/tests/testgit/hooks/update.sample new file mode 100644 index 0000000000000..80ba94135cc37 --- /dev/null +++ b/modules/commitgraph/tests/testgit/hooks/update.sample @@ -0,0 +1,128 @@ +#!/bin/sh +# +# An example hook script to block unannotated tags from entering. +# Called by "git receive-pack" with arguments: refname sha1-old sha1-new +# +# To enable this hook, rename this file to "update". +# +# Config +# ------ +# hooks.allowunannotated +# This boolean sets whether unannotated tags will be allowed into the +# repository. By default they won't be. +# hooks.allowdeletetag +# This boolean sets whether deleting tags will be allowed in the +# repository. By default they won't be. +# hooks.allowmodifytag +# This boolean sets whether a tag may be modified after creation. By default +# it won't be. +# hooks.allowdeletebranch +# This boolean sets whether deleting branches will be allowed in the +# repository. By default they won't be. +# hooks.denycreatebranch +# This boolean sets whether remotely creating branches will be denied +# in the repository. By default this is allowed. +# + +# --- Command line +refname="$1" +oldrev="$2" +newrev="$3" + +# --- Safety check +if [ -z "$GIT_DIR" ]; then + echo "Don't run this script from the command line." >&2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# --- Config +allowunannotated=$(git config --bool hooks.allowunannotated) +allowdeletebranch=$(git config --bool hooks.allowdeletebranch) +denycreatebranch=$(git config --bool hooks.denycreatebranch) +allowdeletetag=$(git config --bool hooks.allowdeletetag) +allowmodifytag=$(git config --bool hooks.allowmodifytag) + +# check for no description +projectdesc=$(sed -e '1q' "$GIT_DIR/description") +case "$projectdesc" in +"Unnamed repository"* | "") + echo "*** Project description file hasn't been set" >&2 + exit 1 + ;; +esac + +# --- Check types +# if $newrev is 0000...0000, it's a commit to delete a ref. +zero="0000000000000000000000000000000000000000" +if [ "$newrev" = "$zero" ]; then + newrev_type=delete +else + newrev_type=$(git cat-file -t $newrev) +fi + +case "$refname","$newrev_type" in + refs/tags/*,commit) + # un-annotated tag + short_refname=${refname##refs/tags/} + if [ "$allowunannotated" != "true" ]; then + echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,delete) + # delete tag + if [ "$allowdeletetag" != "true" ]; then + echo "*** Deleting a tag is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 + then + echo "*** Tag '$refname' already exists." >&2 + echo "*** Modifying a tag is not allowed in this repository." >&2 + exit 1 + fi + ;; + refs/heads/*,commit) + # branch + if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then + echo "*** Creating a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/heads/*,delete) + # delete branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/remotes/*,commit) + # tracking branch + ;; + refs/remotes/*,delete) + # delete tracking branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a tracking branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 + exit 1 + ;; +esac + +# --- Finished +exit 0 diff --git a/modules/commitgraph/tests/testgit/index b/modules/commitgraph/tests/testgit/index new file mode 100644 index 0000000000000000000000000000000000000000..460a647d9523d4d6a0fe55975a6d1b52f5a58edd GIT binary patch literal 217 zcmZ?q402{*U|<5_=oPH$@;)V*%;F>lbeC-Yf60D0ppB`{NrQ zn4;C+wlQ$)rDvAp=BJeAq!vRJ1IQ5F*9>I!5@G8icsa5?{Wx%FX%B)7wPi@<=S8i|7`pOZrJ X^OzJrxlVLb`;=q 1537738486 +0200 commit (initial): Boo diff --git a/modules/commitgraph/tests/testgit/logs/refs/heads/master b/modules/commitgraph/tests/testgit/logs/refs/heads/master new file mode 100644 index 0000000000000..208cd9f99b3f2 --- /dev/null +++ b/modules/commitgraph/tests/testgit/logs/refs/heads/master @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 5aa811d3c2f6d5d6e928a4acacd15248928c26d0 Filip Navara 1537738486 +0200 commit (initial): Boo diff --git a/modules/commitgraph/tests/testgit/objects/43/fb44daf0a8190b40cf385150627c18c164f362 b/modules/commitgraph/tests/testgit/objects/43/fb44daf0a8190b40cf385150627c18c164f362 new file mode 100644 index 0000000000000000000000000000000000000000..65b492f99da7a717b4b3b6ac8278f0c4f83f62ff GIT binary patch literal 88 zcmV-e0H^AWH2-^Ff%bx&`ZxO$<0qG%}Fh0xPIaG?!EF3i-OlCwLiY` ufhk)3ZJVK)0T3vZq!yPj99r#lq_+Lb*>qn?r9PEynb{X_`vL&CJs{+kSSW)4 literal 0 HcmV?d00001 diff --git a/modules/commitgraph/tests/testgit/objects/5a/a811d3c2f6d5d6e928a4acacd15248928c26d0 b/modules/commitgraph/tests/testgit/objects/5a/a811d3c2f6d5d6e928a4acacd15248928c26d0 new file mode 100644 index 0000000000000000000000000000000000000000..55a0bbe9cbb52def7dec661f468cf3c56e110344 GIT binary patch literal 126 zcmV-^0D=E_0iBIO3IZ_<06q5=xeJoC*|vg+c<|^Sva!X&b}Oa&eixtMX<%R&ZMBvG z6zp#Flz?5HnIDo-A{}vN)a>F!6M7qzqLSzAyfyM3&(`5qmhytTe54DU)*mma>Qd4= g4%+G&XmTOA$kD?dEuz`%_8jTI+FV;RU+md7-zw`^cf$>7=CZh%rCrHN|L8V?R?zek*+*9usRR8UO$Q literal 0 HcmV?d00001 diff --git a/modules/commitgraph/tests/testgit/objects/info/commit-graph b/modules/commitgraph/tests/testgit/objects/info/commit-graph new file mode 100644 index 0000000000000000000000000000000000000000..165af086a7d32d846498a8187abb8ac5ec125bf6 GIT binary patch literal 1156 zcmZ>E5Aa}QWM=U9ba7*V01F`72f}2raCUJFfwG}uARbErL~N9#LkKVe?HI+QVKAuD zK-3Ds%ZI*Qz4lUL$(l76gFGhns9kXW?Q-kG3Q2B<^A>>tNi`A&Q$8nw3JPdp!4kcK b?OU3oe-~HVvv&qF_I{ZltuRAB Date: Mon, 22 Apr 2019 00:37:25 +0200 Subject: [PATCH 2/2] Update encoder.go --- modules/commitgraph/plumbing/format/commitgraph/encoder.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/commitgraph/plumbing/format/commitgraph/encoder.go b/modules/commitgraph/plumbing/format/commitgraph/encoder.go index 05d589149b34f..211140f45c4e9 100644 --- a/modules/commitgraph/plumbing/format/commitgraph/encoder.go +++ b/modules/commitgraph/plumbing/format/commitgraph/encoder.go @@ -73,7 +73,7 @@ func (e *Encoder) Encode(idx Index) error { var commitDataOffset = oidLookupOffset + uint64(len(hashes))*20 var bloomOffset = commitDataOffset + uint64(len(hashes))*36 var sparseBloomOffset = bloomOffset + uint64(bloomFiltersCount)*640 - var largeEdgeListOffset uint64 + var largeEdgeListOffset = bloomOffset var largeEdges []uint32 // Write header