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
}