shithub: furgit

ref: d119343e8e7c83a56f87b05249ff0094aa2298a5
dir: /refs.go/

View raw version
package furgit

import (
	"bufio"
	"bytes"
	"fmt"
	"os"
	"slices"
	"strings"
)

func (repo *Repository) resolveLooseRef(refname string) (Ref, error) {
	data, err := os.ReadFile(repo.repoPath(refname))
	if err != nil {
		if os.IsNotExist(err) {
			return Ref{}, ErrNotFound
		}
		return Ref{}, err
	}
	line := strings.TrimSpace(string(data))

	if strings.HasPrefix(line, "ref: ") {
		target := strings.TrimSpace(line[5:])
		if target == "" {
			return Ref{Kind: RefKindInvalid}, ErrInvalidRef
		}
		return Ref{
			Kind: RefKindSymbolic,
			Ref:  target,
		}, nil
	}

	id, err := repo.ParseHash(line)
	if err != nil {
		return Ref{Kind: RefKindInvalid}, err
	}
	return Ref{
		Kind: RefKindDetached,
		Hash: id,
	}, nil
}

func (repo *Repository) resolvePackedRef(refname string) (Ref, error) {
	// According to git-pack-refs(1), symbolic refs are never
	// stored in packed-refs, so we only need to look for detached
	// refs here.

	path := repo.repoPath("packed-refs")
	f, err := os.Open(path)
	if err != nil {
		if os.IsNotExist(err) {
			return Ref{}, ErrNotFound
		}
		return Ref{}, err
	}
	defer func() { _ = f.Close() }()

	want := []byte(refname)
	scanner := bufio.NewScanner(f)

	for scanner.Scan() {
		line := scanner.Bytes()

		if len(line) == 0 || line[0] == '#' || line[0] == '^' {
			continue
		}

		sp := bytes.IndexByte(line, ' ')
		if sp != repo.hashSize*2 {
			continue
		}

		name := line[sp+1:]

		if !bytes.Equal(name, want) {
			continue
		}

		hex := string(line[:sp])
		id, err := repo.ParseHash(hex)
		if err != nil {
			return Ref{Kind: RefKindInvalid}, err
		}

		ref := Ref{
			Kind: RefKindDetached,
			Hash: id,
		}

		if scanner.Scan() {
			next := scanner.Bytes()
			if len(next) > 0 && next[0] == '^' {
				peeledHex := strings.TrimPrefix(string(next), "^")
				peeledHex = strings.TrimSpace(peeledHex)

				peeledID, err := repo.ParseHash(peeledHex)
				if err != nil {
					return Ref{Kind: RefKindInvalid}, err
				}
				ref.Peeled = peeledID
			}
		}

		if scanErr := scanner.Err(); scanErr != nil {
			return Ref{Kind: RefKindInvalid}, scanErr
		}

		return ref, nil
	}

	if scanErr := scanner.Err(); scanErr != nil {
		return Ref{Kind: RefKindInvalid}, scanErr
	}
	return Ref{}, ErrNotFound
}

// RefKind represents the kind of HEAD reference.
type RefKind int

const (
	// The HEAD reference is invalid.
	RefKindInvalid RefKind = iota
	// The HEAD reference points to a detached commit hash.
	RefKindDetached
	// The HEAD reference points to a symbolic ref.
	RefKindSymbolic
)

// Ref represents a reference.
type Ref struct {
	// Kind is the kind of the reference.
	Kind RefKind
	// When Kind is RefKindSymbolic, Ref is the fully qualified ref name.
	// Otherwise the value is undefined.
	Ref string
	// When Kind is RefKindDetached, Hash is the commit hash.
	// Otherwise the value is undefined.
	Hash Hash
	// When Kind is RefKindDetached, and the ref supposedly points to an
	// annotated tag, Peeled is the peeled hash, i.e., the hash of the
	// object that the tag points to.
	Peeled Hash
}

// ResolveRef reads the given fully qualified ref (such as "HEAD" or "refs/heads/main")
// and interprets its contents as either a symbolic ref ("ref: refs/..."), a detached
// hash, or invalid.
// If path is empty, it defaults to "HEAD".
// (While typically only HEAD may be a symbolic reference, others may be as well.)
func (repo *Repository) ResolveRef(path string) (Ref, error) {
	if path == "" {
		path = "HEAD"
	}

	if !strings.HasPrefix(path, "refs/") && !slices.Contains([]string{
		"HEAD", "ORIG_HEAD", "FETCH_HEAD", "MERGE_HEAD",
		"CHERRY_PICK_HEAD", "REVERT_HEAD", "REBASE_HEAD", "BISECT_HEAD",
	}, path) {
		id, err := repo.ParseHash(path)
		if err == nil {
			return Ref{
				Kind: RefKindDetached,
				Hash: id,
			}, nil
		}

		// For now let's keep this to prevent e.g., random users from
		// specifying something crazy like objects/... or ./config.
		// There may be other legal pseudo-refs in the future,
		// but it's probably the best to stay cautious for now.
		return Ref{Kind: RefKindInvalid}, ErrInvalidRef
	}

	loose, err := repo.resolveLooseRef(path)
	if err == nil {
		return loose, nil
	}
	if err != ErrNotFound {
		return Ref{Kind: RefKindInvalid}, err
	}

	packed, err := repo.resolvePackedRef(path)
	if err == nil {
		return packed, nil
	}
	if err != ErrNotFound {
		return Ref{Kind: RefKindInvalid}, err
	}

	return Ref{Kind: RefKindInvalid}, ErrNotFound
}

// ResolveRefFully resolves a ref by recursively following
// symbolic references until it reaches a detached ref.
// Symbolic cycles are detected and reported.
// Tags are not peeled automatically.
func (repo *Repository) ResolveRefFully(path string) (Hash, error) {
	seen := make(map[string]struct{})
	return repo.resolveRefFully(path, seen)
}

func (repo *Repository) resolveRefFully(path string, seen map[string]struct{}) (Hash, error) {
	if _, found := seen[path]; found {
		return Hash{}, fmt.Errorf("symbolic ref cycle involving %q", path)
	}
	seen[path] = struct{}{}

	ref, err := repo.ResolveRef(path)
	if err != nil {
		return Hash{}, err
	}

	switch ref.Kind {
	case RefKindDetached:
		return ref.Hash, nil

	case RefKindSymbolic:
		if ref.Ref == "" {
			return Hash{}, ErrInvalidRef
		}
		return repo.resolveRefFully(ref.Ref, seen)

	default:
		return Hash{}, ErrInvalidRef
	}
}