aboutsummaryrefslogblamecommitdiffstats
path: root/plumbing/format/packfile/packfile.go
blob: 00014f6a5c57a772dedd2e4f12e127c7e78baf3c (plain) (tree)


















                                                                 
                                                    











                                                                            
                                                    















                                                                            


                                                              










                                                       
                                                              
                                        




































































































































                                                                                                 



                               




















                                                                          














                                                                                                 
                               

         
                           











































                                                                                                   



                                           





































                                                                             
                                   








                                                                                      







                                           


























                                                                             

                              


                                                             


                               

         
                                               


















                                                                          
                      
 
package packfile

import (
	"bytes"
	"io"

	billy "gopkg.in/src-d/go-billy.v4"
	"gopkg.in/src-d/go-git.v4/plumbing"
	"gopkg.in/src-d/go-git.v4/plumbing/cache"
	"gopkg.in/src-d/go-git.v4/plumbing/format/idxfile"
	"gopkg.in/src-d/go-git.v4/plumbing/storer"
)

// Packfile allows retrieving information from inside a packfile.
type Packfile struct {
	idxfile.Index
	billy.File
	s              *Scanner
	deltaBaseCache cache.Object
	offsetToType   map[int64]plumbing.ObjectType
}

// NewPackfile returns a packfile representation for the given packfile file
// and packfile idx.
func NewPackfile(index idxfile.Index, file billy.File) *Packfile {
	s := NewScanner(file)

	return &Packfile{
		index,
		file,
		s,
		cache.NewObjectLRUDefault(),
		make(map[int64]plumbing.ObjectType),
	}
}

// Get retrieves the encoded object in the packfile with the given hash.
func (p *Packfile) Get(h plumbing.Hash) (plumbing.EncodedObject, error) {
	offset, err := p.FindOffset(h)
	if err != nil {
		return nil, err
	}

	return p.GetByOffset(offset)
}

// GetByOffset retrieves the encoded object from the packfile with the given
// offset.
func (p *Packfile) GetByOffset(o int64) (plumbing.EncodedObject, error) {
	hash, err := p.FindHash(o)
	if err == nil {
		if obj, ok := p.deltaBaseCache.Get(hash); ok {
			return obj, nil
		}
	}

	if _, err := p.s.SeekFromStart(o); err != nil {
		return nil, err
	}

	return p.nextObject()
}

func (p *Packfile) nextObjectHeader() (*ObjectHeader, error) {
	h, err := p.s.NextObjectHeader()
	p.s.pendingObject = nil
	return h, err
}

func (p *Packfile) getObjectData(
	h *ObjectHeader,
) (typ plumbing.ObjectType, size int64, err error) {
	switch h.Type {
	case plumbing.CommitObject, plumbing.TreeObject, plumbing.BlobObject, plumbing.TagObject:
		typ = h.Type
		size = h.Length
	case plumbing.REFDeltaObject, plumbing.OFSDeltaObject:
		buf := bufPool.Get().(*bytes.Buffer)
		buf.Reset()
		defer bufPool.Put(buf)

		_, _, err = p.s.NextObject(buf)
		if err != nil {
			return
		}

		delta := buf.Bytes()
		_, delta = decodeLEB128(delta) // skip src size
		sz, _ := decodeLEB128(delta)
		size = int64(sz)

		var offset int64
		if h.Type == plumbing.REFDeltaObject {
			offset, err = p.FindOffset(h.Reference)
			if err != nil {
				return
			}
		} else {
			offset = h.OffsetReference
		}

		if baseType, ok := p.offsetToType[offset]; ok {
			typ = baseType
		} else {
			if _, err = p.s.SeekFromStart(offset); err != nil {
				return
			}

			h, err = p.nextObjectHeader()
			if err != nil {
				return
			}

			typ, _, err = p.getObjectData(h)
			if err != nil {
				return
			}
		}
	default:
		err = ErrInvalidObject.AddDetails("type %q", h.Type)
	}

	return
}

func (p *Packfile) getObjectSize(h *ObjectHeader) (int64, error) {
	switch h.Type {
	case plumbing.CommitObject, plumbing.TreeObject, plumbing.BlobObject, plumbing.TagObject:
		return h.Length, nil
	case plumbing.REFDeltaObject, plumbing.OFSDeltaObject:
		buf := bufPool.Get().(*bytes.Buffer)
		buf.Reset()
		defer bufPool.Put(buf)

		if _, _, err := p.s.NextObject(buf); err != nil {
			return 0, err
		}

		delta := buf.Bytes()
		_, delta = decodeLEB128(delta) // skip src size
		sz, _ := decodeLEB128(delta)
		return int64(sz), nil
	default:
		return 0, ErrInvalidObject.AddDetails("type %q", h.Type)
	}
}

func (p *Packfile) getObjectType(h *ObjectHeader) (typ plumbing.ObjectType, err error) {
	switch h.Type {
	case plumbing.CommitObject, plumbing.TreeObject, plumbing.BlobObject, plumbing.TagObject:
		return h.Type, nil
	case plumbing.REFDeltaObject, plumbing.OFSDeltaObject:
		var offset int64
		if h.Type == plumbing.REFDeltaObject {
			offset, err = p.FindOffset(h.Reference)
			if err != nil {
				return
			}
		} else {
			offset = h.OffsetReference
		}

		if baseType, ok := p.offsetToType[offset]; ok {
			typ = baseType
		} else {
			if _, err = p.s.SeekFromStart(offset); err != nil {
				return
			}

			h, err = p.nextObjectHeader()
			if err != nil {
				return
			}

			typ, err = p.getObjectType(h)
			if err != nil {
				return
			}
		}
	default:
		err = ErrInvalidObject.AddDetails("type %q", h.Type)
	}

	return
}

func (p *Packfile) nextObject() (plumbing.EncodedObject, error) {
	h, err := p.nextObjectHeader()
	if err != nil {
		return nil, err
	}

	hash, err := p.FindHash(h.Offset)
	if err != nil {
		return nil, err
	}

	size, err := p.getObjectSize(h)
	if err != nil {
		return nil, err
	}

	typ, err := p.getObjectType(h)
	if err != nil {
		return nil, err
	}

	p.offsetToType[h.Offset] = typ

	return NewDiskObject(hash, typ, h.Offset, size, p), nil
}

func (p *Packfile) getObjectContent(offset int64) (io.ReadCloser, error) {
	if _, err := p.s.SeekFromStart(offset); err != nil {
		return nil, err
	}

	h, err := p.nextObjectHeader()
	if err != nil {
		return nil, err
	}

	var obj = new(plumbing.MemoryObject)
	obj.SetSize(h.Length)
	obj.SetType(h.Type)

	switch h.Type {
	case plumbing.CommitObject, plumbing.TreeObject, plumbing.BlobObject, plumbing.TagObject:
		err = p.fillRegularObjectContent(obj)
	case plumbing.REFDeltaObject:
		err = p.fillREFDeltaObjectContent(obj, h.Reference)
	case plumbing.OFSDeltaObject:
		err = p.fillOFSDeltaObjectContent(obj, h.OffsetReference)
	default:
		err = ErrInvalidObject.AddDetails("type %q", h.Type)
	}

	if err != nil {
		return nil, err
	}

	return obj.Reader()
}

func (p *Packfile) fillRegularObjectContent(obj plumbing.EncodedObject) error {
	w, err := obj.Writer()
	if err != nil {
		return err
	}

	_, _, err = p.s.NextObject(w)
	return err
}

func (p *Packfile) fillREFDeltaObjectContent(obj plumbing.EncodedObject, ref plumbing.Hash) error {
	buf := bufPool.Get().(*bytes.Buffer)
	buf.Reset()
	_, _, err := p.s.NextObject(buf)
	if err != nil {
		return err
	}

	base, ok := p.cacheGet(ref)
	if !ok {
		base, err = p.Get(ref)
		if err != nil {
			return err
		}
	}

	obj.SetType(base.Type())
	err = ApplyDelta(obj, base, buf.Bytes())
	p.cachePut(obj)
	bufPool.Put(buf)

	return err
}

func (p *Packfile) fillOFSDeltaObjectContent(obj plumbing.EncodedObject, offset int64) error {
	buf := bytes.NewBuffer(nil)
	_, _, err := p.s.NextObject(buf)
	if err != nil {
		return err
	}

	var base plumbing.EncodedObject
	var ok bool
	hash, err := p.FindHash(offset)
	if err == nil {
		base, ok = p.cacheGet(hash)
	}

	if !ok {
		base, err = p.GetByOffset(offset)
		if err != nil {
			return err
		}

		p.cachePut(base)
	}

	obj.SetType(base.Type())
	err = ApplyDelta(obj, base, buf.Bytes())
	p.cachePut(obj)

	return err
}

func (p *Packfile) cacheGet(h plumbing.Hash) (plumbing.EncodedObject, bool) {
	if p.deltaBaseCache == nil {
		return nil, false
	}

	return p.deltaBaseCache.Get(h)
}

func (p *Packfile) cachePut(obj plumbing.EncodedObject) {
	if p.deltaBaseCache == nil {
		return
	}

	p.deltaBaseCache.Put(obj)
}

// GetAll returns an iterator with all encoded objects in the packfile.
// The iterator returned is not thread-safe, it should be used in the same
// thread as the Packfile instance.
func (p *Packfile) GetAll() (storer.EncodedObjectIter, error) {
	entries, err := p.Entries()
	if err != nil {
		return nil, err
	}

	return &objectIter{
		// Easiest way to provide an object decoder is just to pass a Packfile
		// instance. To not mess with the seeks, it's a new instance with a
		// different scanner but the same cache and offset to hash map for
		// reusing as much cache as possible.
		p: &Packfile{
			p.Index,
			p.File,
			NewScanner(p.File),
			p.deltaBaseCache,
			p.offsetToType,
		},
		iter: entries,
	}, nil
}

// ID returns the ID of the packfile, which is the checksum at the end of it.
func (p *Packfile) ID() (plumbing.Hash, error) {
	if _, err := p.File.Seek(-20, io.SeekEnd); err != nil {
		return plumbing.ZeroHash, err
	}

	var hash plumbing.Hash
	if _, err := io.ReadFull(p.File, hash[:]); err != nil {
		return plumbing.ZeroHash, err
	}

	return hash, nil
}

// Close the packfile and its resources.
func (p *Packfile) Close() error {
	return p.File.Close()
}

type objectDecoder interface {
	nextObject() (plumbing.EncodedObject, error)
}

type objectIter struct {
	p    *Packfile
	iter idxfile.EntryIter
}

func (i *objectIter) Next() (plumbing.EncodedObject, error) {
	e, err := i.iter.Next()
	if err != nil {
		return nil, err
	}

	return i.p.GetByOffset(int64(e.Offset))
}

func (i *objectIter) ForEach(f func(plumbing.EncodedObject) error) error {
	for {
		o, err := i.Next()
		if err != nil {
			if err == io.EOF {
				return nil
			}
			return err
		}

		if err := f(o); err != nil {
			return err
		}
	}
}

func (i *objectIter) Close() {
	i.iter.Close()
}