7.3 KB
git.go
package sharing

import (
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"time"

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

const (
	// RepoPath is the path to the bare git repository.
	RepoPath = "/var/git/readysite.git"
)

// TreeEntry represents a file or directory in the git tree.
type TreeEntry struct {
	Name  string
	Path  string
	IsDir bool
	Size  int64
	Mode  string
}

// Commit represents a git commit.
type Commit struct {
	Hash      string
	ShortHash string
	Message   string
	Author    string
	Email     string
	Date      time.Time
}

// InitRepo creates a bare git repository at RepoPath if it doesn't exist.
func InitRepo() error {
	// Check if repo already exists
	if _, err := os.Stat(filepath.Join(RepoPath, "HEAD")); err == nil {
		return nil // Already initialized
	}

	// Create parent directory
	if err := os.MkdirAll(filepath.Dir(RepoPath), 0755); err != nil {
		return fmt.Errorf("create git dir: %w", err)
	}

	// Initialize bare repository
	cmd := exec.Command("git", "init", "--bare", RepoPath)
	if output, err := cmd.CombinedOutput(); err != nil {
		return fmt.Errorf("git init: %w: %s", err, output)
	}

	// Set ownership to git user (if it exists)
	if _, err := exec.Command("id", "git").Output(); err == nil {
		cmd = exec.Command("chown", "-R", "git:git", RepoPath)
		if output, err := cmd.CombinedOutput(); err != nil {
			return fmt.Errorf("chown: %w: %s", err, output)
		}
	}

	return nil
}

// InstallPostReceiveHook installs a post-receive hook for future build triggers.
func InstallPostReceiveHook() error {
	hookPath := filepath.Join(RepoPath, "hooks", "post-receive")

	// Check if hook already exists
	if _, err := os.Stat(hookPath); err == nil {
		return nil // Already installed
	}

	// Create hooks directory if needed
	if err := os.MkdirAll(filepath.Dir(hookPath), 0755); err != nil {
		return fmt.Errorf("create hooks dir: %w", err)
	}

	// Write hook script
	hook := `#!/bin/sh
# Post-receive hook for readysite.git
# This hook is called after a successful push.
# Add build triggers here in the future.

echo "Received push to readysite.git"
`
	if err := os.WriteFile(hookPath, []byte(hook), 0755); err != nil {
		return fmt.Errorf("write hook: %w", err)
	}

	return nil
}

// CloneURL returns the git clone URL for the given host.
func CloneURL(host string) string {
	return fmt.Sprintf("https://%s/git/readysite.git", host)
}

// OpenRepo opens the git repository for reading.
func OpenRepo() (*git.Repository, error) {
	return git.PlainOpen(RepoPath)
}

// HasRepo checks if the repository exists and has commits.
func HasRepo() bool {
	repo, err := OpenRepo()
	if err != nil {
		return false
	}
	head, err := repo.Head()
	if err != nil {
		return false
	}
	return head != nil
}

// GetTree returns the tree entries at the given path.
func GetTree(path string) ([]TreeEntry, error) {
	repo, err := OpenRepo()
	if err != nil {
		return nil, fmt.Errorf("open repo: %w", err)
	}

	head, err := repo.Head()
	if err != nil {
		return nil, fmt.Errorf("get head: %w", err)
	}

	commit, err := repo.CommitObject(head.Hash())
	if err != nil {
		return nil, fmt.Errorf("get commit: %w", err)
	}

	tree, err := commit.Tree()
	if err != nil {
		return nil, fmt.Errorf("get tree: %w", err)
	}

	// Navigate to path if specified
	if path != "" && path != "/" {
		tree, err = tree.Tree(path)
		if err != nil {
			return nil, fmt.Errorf("get subtree: %w", err)
		}
	}

	var entries []TreeEntry
	for _, entry := range tree.Entries {
		e := TreeEntry{
			Name:  entry.Name,
			Path:  filepath.Join(path, entry.Name),
			IsDir: entry.Mode.IsFile() == false,
			Mode:  entry.Mode.String(),
		}

		if entry.Mode.IsFile() {
			file, err := tree.TreeEntryFile(&entry)
			if err == nil {
				e.Size = file.Size
			}
		}

		entries = append(entries, e)
	}

	// Sort: directories first, then alphabetically
	sort.Slice(entries, func(i, j int) bool {
		if entries[i].IsDir != entries[j].IsDir {
			return entries[i].IsDir
		}
		return entries[i].Name < entries[j].Name
	})

	return entries, nil
}

// GetFile returns the content of a file at the given path.
func GetFile(path string) (string, error) {
	repo, err := OpenRepo()
	if err != nil {
		return "", fmt.Errorf("open repo: %w", err)
	}

	head, err := repo.Head()
	if err != nil {
		return "", fmt.Errorf("get head: %w", err)
	}

	commit, err := repo.CommitObject(head.Hash())
	if err != nil {
		return "", fmt.Errorf("get commit: %w", err)
	}

	file, err := commit.File(path)
	if err != nil {
		return "", fmt.Errorf("get file: %w", err)
	}

	reader, err := file.Reader()
	if err != nil {
		return "", fmt.Errorf("read file: %w", err)
	}
	defer reader.Close()

	content, err := io.ReadAll(reader)
	if err != nil {
		return "", fmt.Errorf("read content: %w", err)
	}

	return string(content), nil
}

// GetFileSize returns the size of a file.
func GetFileSize(path string) (int64, error) {
	repo, err := OpenRepo()
	if err != nil {
		return 0, err
	}

	head, err := repo.Head()
	if err != nil {
		return 0, err
	}

	commit, err := repo.CommitObject(head.Hash())
	if err != nil {
		return 0, err
	}

	file, err := commit.File(path)
	if err != nil {
		return 0, err
	}

	return file.Size, nil
}

// GetCommits returns recent commits.
func GetCommits(limit int) ([]Commit, error) {
	repo, err := OpenRepo()
	if err != nil {
		return nil, fmt.Errorf("open repo: %w", err)
	}

	head, err := repo.Head()
	if err != nil {
		return nil, fmt.Errorf("get head: %w", err)
	}

	iter, err := repo.Log(&git.LogOptions{
		From:  head.Hash(),
		Order: git.LogOrderCommitterTime,
	})
	if err != nil {
		return nil, fmt.Errorf("get log: %w", err)
	}

	var commits []Commit
	count := 0
	err = iter.ForEach(func(c *object.Commit) error {
		if count >= limit {
			return io.EOF
		}
		commits = append(commits, Commit{
			Hash:      c.Hash.String(),
			ShortHash: c.Hash.String()[:7],
			Message:   c.Message,
			Author:    c.Author.Name,
			Email:     c.Author.Email,
			Date:      c.Author.When,
		})
		count++
		return nil
	})
	if err != nil && err != io.EOF {
		return nil, fmt.Errorf("iterate commits: %w", err)
	}

	return commits, nil
}

// GetBranch returns the current branch name.
func GetBranch() string {
	repo, err := OpenRepo()
	if err != nil {
		return "main"
	}

	head, err := repo.Head()
	if err != nil {
		return "main"
	}

	if head.Name().IsBranch() {
		return head.Name().Short()
	}

	return "main"
}

// GetCommitCount returns the total number of commits.
func GetCommitCount() int {
	repo, err := OpenRepo()
	if err != nil {
		return 0
	}

	head, err := repo.Head()
	if err != nil {
		return 0
	}

	iter, err := repo.Log(&git.LogOptions{From: head.Hash()})
	if err != nil {
		return 0
	}

	count := 0
	iter.ForEach(func(c *object.Commit) error {
		count++
		return nil
	})

	return count
}

// GetLastCommit returns the most recent commit.
func GetLastCommit() (*Commit, error) {
	commits, err := GetCommits(1)
	if err != nil {
		return nil, err
	}
	if len(commits) == 0 {
		return nil, fmt.Errorf("no commits")
	}
	return &commits[0], nil
}

// ResolveRef resolves a reference name to a hash.
func ResolveRef(ref string) (plumbing.Hash, error) {
	repo, err := OpenRepo()
	if err != nil {
		return plumbing.ZeroHash, err
	}

	hash, err := repo.ResolveRevision(plumbing.Revision(ref))
	if err != nil {
		return plumbing.ZeroHash, err
	}

	return *hash, nil
}
← Back