fierj
PublicTiny 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)