shithub: furgit

ref: 565bee5877265ac4e58f3970bb2a8d1195177537
dir: /repo.go/

View raw version
package furgit

import (
	"encoding/hex"
	"fmt"
	"os"
	"path/filepath"
	"sync"

	"codeberg.org/lindenii/furgit/config"
)

// Repository represents a Git repository.
//
// It is safe to access the same Repository from multiple goroutines
// without additional synchronization.
//
// Objects derived from a Repository must not be used after the Repository
// has been closed.
type Repository struct {
	rootPath string
	hashAlgo hashAlgorithm

	packIdxOnce sync.Once
	packIdx     []*packIndex
	packIdxErr  error

	packFiles   map[string]*packFile
	packFilesMu sync.RWMutex

	commitGraphOnce sync.Once
	commitGraph     *commitGraph
	commitGraphErr  error

	closeOnce sync.Once
}

// OpenRepository opens the repository at the provided path.
//
// The path is expected to be the actual repository directory, i.e.,
// the repository itself for bare repositories, or the .git
// subdirectory for non-bare repositories.
func OpenRepository(path string) (*Repository, error) {
	fi, err := os.Stat(path)
	if err != nil {
		return nil, err
	}
	if !fi.IsDir() {
		return nil, ErrInvalidObject
	}

	cfgPath := filepath.Join(path, "config")
	f, err := os.Open(cfgPath)
	if err != nil {
		return nil, fmt.Errorf("furgit: unable to open config: %w", err)
	}
	defer func() {
		_ = f.Close()
	}()

	cfg, err := config.ParseConfig(f)
	if err != nil {
		return nil, fmt.Errorf("furgit: failed to parse config: %w", err)
	}

	algo := cfg.Get("extensions", "", "objectformat")
	if algo == "" {
		algo = "sha1"
	}

	hashAlgo, ok := parseHashAlgorithm(algo)
	if !ok {
		return nil, fmt.Errorf("furgit: unsupported hash algorithm %q", algo)
	}

	return &Repository{
		rootPath:  path,
		hashAlgo:  hashAlgo,
		packFiles: make(map[string]*packFile),
	}, nil
}

// Close closes the repository, releasing any resources associated with it.
//
// It is safe to call Close multiple times; subsequent calls will have no
// effect.
//
// Close invalidates any objects derived from the Repository as it;
// using them may cause segmentation faults or other undefined behavior.
func (repo *Repository) Close() error {
	var closeErr error
	repo.closeOnce.Do(func() {
		repo.packFilesMu.Lock()
		for key, pf := range repo.packFiles {
			err := pf.Close()
			if err != nil && closeErr == nil {
				closeErr = err
			}
			delete(repo.packFiles, key)
		}
		repo.packFilesMu.Unlock()
		if len(repo.packIdx) > 0 {
			for _, idx := range repo.packIdx {
				err := idx.Close()
				if err != nil && closeErr == nil {
					closeErr = err
				}
			}
		}
		if repo.commitGraph != nil {
			err := repo.commitGraph.Close()
			if err != nil && closeErr == nil {
				closeErr = err
			}
		}
	})
	return closeErr
}

// repoPath joins the root with a relative path.
func (repo *Repository) repoPath(rel string) string {
	return filepath.Join(repo.rootPath, rel)
}

// ParseHash converts a hex string into a Hash, validating
// it matches the repository's hash size.
func (repo *Repository) ParseHash(s string) (Hash, error) {
	var id Hash
	if len(s)%2 != 0 {
		return id, fmt.Errorf("furgit: invalid hash length %d, it has to be even at the very least", len(s))
	}
	expectedLen := repo.hashAlgo.Size() * 2
	if len(s) != expectedLen {
		return id, fmt.Errorf("furgit: hash length mismatch: got %d chars, expected %d for hash size %d", len(s), expectedLen, repo.hashAlgo.Size())
	}
	data, err := hex.DecodeString(s)
	if err != nil {
		return id, fmt.Errorf("furgit: decode hash: %w", err)
	}
	copy(id.data[:], data)
	id.algo = repo.hashAlgo
	return id, nil
}

// computeRawHash computes a hash from raw data using the repository's hash algorithm.
func (repo *Repository) computeRawHash(data []byte) Hash {
	return repo.hashAlgo.Sum(data)
}

// verifyRawObject verifies a raw object against its expected hash.
func (repo *Repository) verifyRawObject(buf []byte, want Hash) bool { //nolint:unused
	if want.algo != repo.hashAlgo {
		return false
	}
	return repo.computeRawHash(buf) == want
}

// verifyTypedObject verifies a typed object against its expected hash.
func (repo *Repository) verifyTypedObject(ty ObjectType, body []byte, want Hash) bool { //nolint:unused
	if want.algo != repo.hashAlgo {
		return false
	}
	header, err := headerForType(ty, body)
	if err != nil {
		return false
	}
	raw := make([]byte, len(header)+len(body))
	copy(raw, header)
	copy(raw[len(header):], body)
	return repo.computeRawHash(raw) == want
}