bidet / internal / core / repo.go

@ ff7903274f2065077e05ba4acc12fe480e8bd82f | history


package core

import (
	"errors"
	"os"
	"path"
	"strings"

	"github.com/go-git/go-git/v6"
	"github.com/go-git/go-git/v6/plumbing"
	"github.com/go-git/go-git/v6/plumbing/object"
)

const dotGit = ".git"

func List(dir string) ([]*Repo, error) {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return nil, err
	}
	var repos []*Repo
	for _, e := range entries {
		if e.Name() == dotGit {
			continue
		}
		repo, err := Open(strings.TrimSuffix(e.Name(), dotGit))
		if err == nil {
			if _, err = repo.Worktree(); errors.Is(err, git.ErrIsBareRepository) {
				repos = append(repos, repo)
			}
		}
	}
	if len(repos) == 0 {
		return nil, errors.New("No repos found in " + dir)
	}
	return repos, nil
}

type Repo struct {
	*git.Repository
	Name string
}

func Open(repoName string) (*Repo, error) {
	if _, err := os.Stat(repoName + dotGit); os.IsNotExist(err) {
		return nil, err
	}
	repo, err := git.PlainOpen(repoName + dotGit)
	if err != nil {
		return nil, err
	}
	return &Repo{repo, repoName}, nil
}

func (repo *Repo) Load(entry *object.TreeEntry) (string, error) {
	file, err := repo.BlobObject(entry.Hash)
	if err != nil {
		return "", err
	}
	reader, err := file.Reader()
	if err != nil {
		return "", err
	}
	defer reader.Close()

	content := make([]byte, file.Size)
	reader.Read(content)
	return string(content), nil
}

type Commit struct {
	*object.Commit
	Hash    plumbing.Hash
	RefName string
}

func (repo *Repo) ParseRoute(route string) (*Commit, string, error) {
	if route == "" {
		return nil, "", errors.New("empty route")
	}
	candidate := strings.TrimPrefix(route, "/")
	for {
		ref, err := repo.resolveRef(candidate)
		var relPath string
		if err == nil { // candidate is a tag or a branch name
			if len(candidate) < len(route) {
				relPath = strings.TrimPrefix(route[len(candidate):], "/")
			}
			refName := ref.Name().Short()
			commit, _ := repo.resolveCommit(refName)
			return &Commit{commit, commit.Hash, refName}, relPath, nil
		}
		commit, err := repo.resolveCommit(candidate)
		if err == nil { // candidate is a commit hash
			if len(candidate) < len(route) {
				relPath = strings.TrimPrefix(route[len(candidate):], "/")
			}
			return &Commit{commit, commit.Hash, ""}, relPath, nil
		}
		parent := path.Dir(candidate)
		if parent == "." || parent == candidate {
			break
		}
		candidate = parent
	}
	return nil, "", errors.New("no commit found")
}

func (repo *Repo) resolveCommit(name string) (*object.Commit, error) {
	hash, err := repo.ResolveRevision(plumbing.Revision(name))
	if err != nil {
		return nil, err
	}
	return repo.CommitObject(*hash)
}

func (repo *Repo) resolveRef(name string) (*plumbing.Reference, error) {
	// first check for branches with the given name
	branchRef := plumbing.ReferenceName(path.Join("refs", "heads", name))
	ref, err := repo.Reference(branchRef, true)
	if err == nil {
		return ref, nil
	}
	// then tags
	tagRef := plumbing.ReferenceName(path.Join("refs", "tags", name))
	ref, err = repo.Reference(tagRef, true)
	if err != nil {
		return nil, err
	}
	obj, err := repo.Object(plumbing.AnyObject, ref.Hash())
	if err != nil {
		return nil, err
	}
	// if it's an annotated tag, extract the target
	if tagObj, ok := obj.(*object.Tag); ok {
		ref = plumbing.NewHashReference(ref.Name(), tagObj.Target)
	}
	return ref, nil
}