+ New

fierj

Public

Tiny personal git forge

d1b83edef62102f726006de88feae04b6d238106
diff --git a/config.go b/config.go
index 188d3fb..c66c41a 100644
--- a/config.go
+++ b/config.go
@@ -11,12 +11,14 @@ const envPrefix = "FIERJ_"
 
 type Config struct {
 	Addr string `json:"addr" env:"ADDR"`
+	Dir  string `json:"dir" env:"DIR"`
 }
 
 func LoadConfig(path string) Config {
 	// default config
 	cfg := Config{
 		Addr: ":8080",
+		Dir:  "repos",
 	}
 
 	// read file, log read/unmarshal errors, if any
@@ -33,11 +35,9 @@ func LoadConfig(path string) Config {
 
 	// scan fields and override with env vars (FIERJ_<env>) if present
 	v := reflect.ValueOf(&cfg).Elem()
-	slog.Info("scanning config fields for env overrides", "x", v)
 	for i := 0; i < v.NumField(); i++ {
 		field := v.Type().Field(i)
 		envTag := field.Tag.Get("env")
-		slog.Info("checking field for env override", "field", field, "env", envTag)
 		if envTag == "" {
 			continue
 		}
diff --git a/git.go b/git.go
new file mode 100644
index 0000000..56dcda9
--- /dev/null
+++ b/git.go
@@ -0,0 +1,221 @@
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"os/exec"
+	"path"
+	"path/filepath"
+	"strings"
+)
+
+type TreeEntry struct {
+	Mode string
+	Type string // "blob" or "tree"
+	Hash string
+	Size string
+	Name string
+}
+
+type Commit struct {
+	Hash    string
+	Author  string
+	Date    string
+	Message string
+}
+
+type VCS interface {
+	List(ref, path string) ([]TreeEntry, error)
+	Blob(ref, path string) (string, error)
+	Log(ref string, n int) ([]Commit, error)
+	Diff(hash string) (string, error)
+	DefaultBranch() string
+	IsEmpty() bool
+	Branches() []string
+	Tags() []string
+	CommitCount(ref string) int
+	Readme(ref, dir string) string
+	LastCommit(ref, path string) *Commit
+}
+
+type Git struct {
+	Dir string
+}
+
+var _ VCS = (*Git)(nil)
+
+func (g *Git) cmd(args ...string) (string, error) {
+	cmd := exec.Command("git", args...)
+	cmd.Dir = g.Dir
+	var stdout, stderr bytes.Buffer
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+	if err := cmd.Run(); err != nil {
+		return "", fmt.Errorf("git %v failed: %v, stderr: %s", args, err, stderr.String())
+	}
+	return stdout.String(), nil
+}
+
+func InitRepo(cfg *Config, name string) error {
+	if strings.Contains(name, "/") || strings.Contains(name, "..") {
+		return fmt.Errorf("invalid repo name")
+	}
+	absRoot, err := filepath.Abs(cfg.Dir)
+	if err != nil {
+		return err
+	}
+	repoPath := filepath.Join(absRoot, name+".git")
+	g := &Git{Dir: absRoot}
+	_, err = g.cmd("init", "--bare", repoPath)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (g *Git) List(ref, path string) ([]TreeEntry, error) {
+	args := []string{"ls-tree", "-l", ref}
+	if path != "" {
+		args = append(args, "--", path+"/")
+	}
+	out, err := g.cmd(args...)
+	if err != nil {
+		return nil, err
+	}
+	var entries []TreeEntry
+	for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
+		if line == "" {
+			continue
+		}
+		// format: <mode> <type> <hash> <size>\t<name>
+		parts := strings.SplitN(line, "\t", 2)
+		if len(parts) != 2 {
+			continue
+		}
+		meta := strings.Fields(parts[0])
+		if len(meta) < 4 {
+			continue
+		}
+		name := parts[1]
+		if path != "" {
+			name = strings.TrimPrefix(name, path+"/")
+		}
+		entries = append(entries, TreeEntry{
+			Mode: meta[0],
+			Type: meta[1],
+			Hash: meta[2],
+			Size: meta[3],
+			Name: name,
+		})
+	}
+	return entries, nil
+}
+
+func (g *Git) Blob(ref, path string) (string, error) {
+	return g.cmd("show", ref+":"+path)
+}
+
+func (g *Git) Log(ref string, n int) ([]Commit, error) {
+	format := "%H%n%an%n%aI%n%s%n---"
+	out, err := g.cmd("log", fmt.Sprintf("-%d", n), "--format="+format, ref)
+	if err != nil {
+		return nil, err
+	}
+	var commits []Commit
+	blocks := strings.Split(strings.TrimSpace(out), "---")
+	for _, block := range blocks {
+		lines := strings.Split(strings.TrimSpace(block), "\n")
+		if len(lines) < 4 {
+			continue
+		}
+		commits = append(commits, Commit{
+			Hash:    lines[0],
+			Author:  lines[1],
+			Date:    lines[2],
+			Message: lines[3],
+		})
+	}
+	return commits, nil
+}
+
+func (g *Git) Diff(hash string) (string, error) {
+	return g.cmd("diff-tree", "-p", hash)
+}
+
+func (g *Git) DefaultBranch() string {
+	out, err := g.cmd("symbolic-ref", "--short", "HEAD")
+	if err != nil {
+		return "main"
+	}
+	return strings.TrimSpace(out)
+}
+
+func (g *Git) IsEmpty() bool {
+	_, err := g.cmd("rev-parse", "--verify", "HEAD")
+	return err != nil
+}
+
+func (g *Git) Branches() []string {
+	out, err := g.cmd("for-each-ref", "--format=%(refname:short)", "refs/heads/")
+	if err != nil {
+		return nil
+	}
+	var branches []string
+	for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
+		if line != "" {
+			branches = append(branches, line)
+		}
+	}
+	return branches
+}
+
+func (g *Git) Tags() []string {
+	out, err := g.cmd("for-each-ref", "--sort=-creatordate", "--format=%(refname:short)", "refs/tags/")
+	if err != nil {
+		return nil
+	}
+	var tags []string
+	for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
+		if line != "" {
+			tags = append(tags, line)
+		}
+	}
+	return tags
+}
+
+func (g *Git) CommitCount(ref string) int {
+	out, err := g.cmd("rev-list", "--count", ref)
+	if err != nil {
+		return 0
+	}
+	n := 0
+	fmt.Sscanf(strings.TrimSpace(out), "%d", &n)
+	return n
+}
+
+func (g *Git) Readme(ref, dir string) string {
+	names := []string{"README.md", "readme.md", "README", "README.txt"}
+	for _, name := range names {
+		p := name
+		if dir != "" {
+			p = path.Join(dir, name)
+		}
+		content, err := g.Blob(ref, p)
+		if err == nil {
+			return content
+		}
+	}
+	return ""
+}
+
+func (g *Git) LastCommit(ref, path string) *Commit {
+	out, err := g.cmd("log", "-1", "--format=%H%n%an%n%ar%n%s", ref, "--", path)
+	if err != nil {
+		return nil
+	}
+	lines := strings.SplitN(strings.TrimSpace(out), "\n", 4)
+	if len(lines) < 4 {
+		return nil
+	}
+	return &Commit{Hash: lines[0], Author: lines[1], Date: lines[2], Message: lines[3]}
+}
diff --git a/main.go b/main.go
index a4e47c9..093219f 100644
--- a/main.go
+++ b/main.go
@@ -4,6 +4,7 @@ import (
 	"log/slog"
 	"net/http"
 	"os"
+	"path"
 )
 
 func main() {
@@ -13,6 +14,14 @@ func main() {
 	}
 	cfg := LoadConfig(cfgPath)
 
+	os.MkdirAll(cfg.Dir, 0755)
+
+	if err := InitRepo(&cfg, "demo"); err != nil {
+		slog.Error("failed to init demo repo", "error", err)
+	}
+	g := &Git{Dir: path.Join(cfg.Dir, "demo")}
+	slog.Info("repo", "empty", g.IsEmpty())
+
 	slog.Info("Starting server", "addr", cfg.Addr)
 	if err := http.ListenAndServe(cfg.Addr, nil); err != nil {
 		slog.Error("Server exited", "error", err)