+ New

fierj

Public

Tiny personal git forge

2f38112e9720cd4cf64ab4d96baf6d06d357e91b
diff --git a/git.go b/git.go
index ecc4db4..c739300 100644
--- a/git.go
+++ b/git.go
@@ -3,7 +3,6 @@ package main
 import (
 	"bytes"
 	"fmt"
-	"log/slog"
 	"os/exec"
 	"path"
 	"path/filepath"
@@ -40,15 +39,15 @@ type VCS interface {
 }
 
 type Git struct {
-	Dir string
+	Dir  string
+	Name string
 }
 
 var _ VCS = (*Git)(nil)
 
 func (g *Git) cmd(args ...string) (string, error) {
-	slog.Info("git command", "dir", g.Dir, "args", args)
 	cmd := exec.Command("git", args...)
-	cmd.Dir = g.Dir
+	cmd.Dir = path.Join(g.Dir, g.Name+".git")
 	var stdout, stderr bytes.Buffer
 	cmd.Stdout = &stdout
 	cmd.Stderr = &stderr
diff --git a/handlers.go b/handlers.go
new file mode 100644
index 0000000..2a582b9
--- /dev/null
+++ b/handlers.go
@@ -0,0 +1,113 @@
+package main
+
+import (
+	"net/http"
+	"sort"
+	"text/template"
+)
+
+func renderError(w http.ResponseWriter, tmpl *template.Template, code int, message string) {
+	w.WriteHeader(code)
+	tmpl.ExecuteTemplate(w, "error.html", map[string]any{
+		"Code":    code,
+		"Message": message,
+	})
+}
+
+func Tree(cfg Config, tmpl *template.Template) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		type FileEntry struct {
+			TreeEntry
+			LastCommit *Commit
+		}
+		repoName := r.PathValue("repo")
+		treePath := r.PathValue("path")
+		git := &Git{Dir: cfg.Dir, Name: repoName}
+		ref := r.PathValue("ref")
+		if ref == "" {
+			ref = git.DefaultBranch()
+		}
+		if git.IsEmpty() {
+			tmpl.ExecuteTemplate(w, "tree.html", map[string]any{
+				"Repo":    repoName,
+				"Ref":     ref,
+				"Path":    "",
+				"Entries": []FileEntry{},
+				// "CloneURL":    fmt.Sprintf("%s@%s:%s.git", cfg.SSHUser, cfg.Host, repoName),
+				// "Description": repoDescription(repoPath),
+				"Branches": []string{},
+				"Tags":     []string{},
+				"Commits":  0,
+				"Empty":    true,
+			})
+			return
+		}
+		entries, err := git.List(ref, treePath)
+		if err != nil {
+			renderError(w, tmpl, http.StatusInternalServerError, "failed to list tree: "+err.Error())
+			return
+		}
+		// directories first
+		sort.Slice(entries, func(i, j int) bool {
+			if entries[i].Type != entries[j].Type {
+				return entries[i].Type == "tree"
+			}
+			return entries[i].Name < entries[j].Name
+		})
+
+		fileEntries := []FileEntry{}
+		for _, e := range entries {
+			fp := e.Name
+			if treePath != "" {
+				fp = treePath + "/" + e.Name
+			}
+			fe := FileEntry{TreeEntry: e, LastCommit: git.LastCommit(ref, fp)}
+			fileEntries = append(fileEntries, fe)
+		}
+		readme := git.Readme(ref, treePath)
+		tmpl.ExecuteTemplate(w, "tree.html", map[string]any{
+			"Repo": repoName,
+			"Ref":  ref,
+			"Path": treePath,
+			// "Breadcrumb": makeBreadcrumb(treePath),
+			"Entries": fileEntries,
+			// "CloneURL":    fmt.Sprintf("%s@%s:%s.git", cfg.SSHUser, cfg.Host, repoName),
+			// "Description": repoDescription(repoPath),
+			// "Branches":    listBranches(repoPath),
+			// "Tags":        listTags(repoPath),
+			// "Commits":     commitCount(repoPath, ref),
+			"Readme":    readme,
+			"ActiveTab": "code",
+		})
+	}
+}
+
+func Blob(cfg Config, tmpl *template.Template) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		repoName := r.PathValue("repo")
+		filePath := r.PathValue("path")
+		git := &Git{Dir: cfg.Dir, Name: repoName}
+		ref := r.PathValue("ref")
+		if ref == "" {
+			ref = git.DefaultBranch()
+		}
+
+		content, err := git.Blob(ref, filePath)
+		if err != nil {
+			renderError(w, tmpl, http.StatusNotFound, err.Error())
+			return
+		}
+		tmpl.ExecuteTemplate(w, "blob.html", map[string]any{
+			"Repo": repoName,
+			"Ref":  ref,
+			"Path": filePath,
+			// "Breadcrumb":  makeBreadcrumb(filePath),
+			"Content": content,
+			// "Description": repoDescription(repoPath),
+			// "Branches":    listBranches(repoPath),
+			// "Tags":        listTags(repoPath),
+			// "Commits":     commitCount(repoPath, ref),
+			"ActiveTab": "code",
+		})
+	}
+}
diff --git a/main.go b/main.go
index fb7a2c1..29bf068 100644
--- a/main.go
+++ b/main.go
@@ -8,8 +8,6 @@ import (
 	"net/http"
 	"os"
 	"os/signal"
-	"path"
-	"sort"
 	"strings"
 	"syscall"
 	"text/template"
@@ -75,14 +73,6 @@ var funcMap = template.FuncMap{
 	},
 }
 
-func renderError(w http.ResponseWriter, tmpl *template.Template, code int, message string) {
-	w.WriteHeader(code)
-	tmpl.ExecuteTemplate(w, "error.html", map[string]any{
-		"Code":    code,
-		"Message": message,
-	})
-}
-
 func main() {
 	cfgPath := "config.json"
 	if len(os.Args) > 1 {
@@ -96,68 +86,9 @@ func main() {
 
 	mux := http.NewServeMux()
 
-	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-		repoName := "demo.git"
-		ref := "main"
-		treePath := ""
-		type FileEntry struct {
-			TreeEntry
-			LastCommit *Commit
-		}
-		git := &Git{Dir: path.Join(cfg.Dir, repoName)}
-		if git.IsEmpty() {
-			tmpl.ExecuteTemplate(w, "tree.html", map[string]any{
-				"Repo":    repoName,
-				"Ref":     ref,
-				"Path":    "",
-				"Entries": []FileEntry{},
-				// "CloneURL":    fmt.Sprintf("%s@%s:%s.git", cfg.SSHUser, cfg.Host, repoName),
-				// "Description": repoDescription(repoPath),
-				"Branches": []string{},
-				"Tags":     []string{},
-				"Commits":  0,
-				"Empty":    true,
-			})
-			return
-		}
-		entries, err := git.List(ref, treePath)
-		if err != nil {
-			renderError(w, tmpl, http.StatusInternalServerError, "failed to list tree: "+err.Error())
-			return
-		}
-		// directories first
-		sort.Slice(entries, func(i, j int) bool {
-			if entries[i].Type != entries[j].Type {
-				return entries[i].Type == "tree"
-			}
-			return entries[i].Name < entries[j].Name
-		})
-
-		fileEntries := []FileEntry{}
-		for _, e := range entries {
-			fp := e.Name
-			if treePath != "" {
-				fp = treePath + "/" + e.Name
-			}
-			fe := FileEntry{TreeEntry: e, LastCommit: git.LastCommit(ref, fp)}
-			fileEntries = append(fileEntries, fe)
-		}
-		readme := git.Readme(ref, treePath)
-		tmpl.ExecuteTemplate(w, "tree.html", map[string]any{
-			"Repo": repoName,
-			"Ref":  ref,
-			"Path": treePath,
-			// "Breadcrumb": makeBreadcrumb(treePath),
-			"Entries": fileEntries,
-			// "CloneURL":    fmt.Sprintf("%s@%s:%s.git", cfg.SSHUser, cfg.Host, repoName),
-			// "Description": repoDescription(repoPath),
-			// "Branches":    listBranches(repoPath),
-			// "Tags":        listTags(repoPath),
-			// "Commits":     commitCount(repoPath, ref),
-			"Readme":    readme,
-			"ActiveTab": "code",
-		})
-	})
+	mux.HandleFunc("/{repo}", Tree(cfg, tmpl))
+	mux.HandleFunc("/{repo}/tree/{ref}/{path...}", Tree(cfg, tmpl))
+	mux.HandleFunc("/{repo}/blob/{ref}/{path...}", Blob(cfg, tmpl))
 
 	srv := &http.Server{
 		Addr:    cfg.Addr,
diff --git a/templates/blob.html b/templates/blob.html
new file mode 100644
index 0000000..ed71103
--- /dev/null
+++ b/templates/blob.html
@@ -0,0 +1,18 @@
+{{template "head" .}}
+{{define "title"}}{{.Path}} - {{.Repo}} - fierj{{end}}
+{{template "repo-header" .}}
+{{template "repo-tabs" .}}
+<div style="margin-bottom:var(--space-md);font-size:0.85rem;color:var(--text-secondary);">
+    <a href="/{{.Repo}}/tree/{{.Ref}}">← {{.Repo}}</a>
+    {{range .Breadcrumb}} / <a href="/{{$.Repo}}/tree/{{$.Ref}}/{{.URL}}">{{.Name}}</a>{{end}}
+</div>
+{{if or (hasSuffix .Path ".md") (hasSuffix .Path ".markdown")}}
+<div class="readme-box">
+    <div class="readme-box-header">{{.Path}}</div>
+    <div class="readme-box-body markdown-body">{{.Content}}</div>
+</div>
+{{else}}
+<pre><code>{{.Content}}</code></pre>
+{{end}}
+{{template "foot" .}}
+
diff --git a/templates/layout.html b/templates/layout.html
index 39ac605..dbfa22e 100644
--- a/templates/layout.html
+++ b/templates/layout.html
@@ -913,6 +913,26 @@
     <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/highlight.min.js"></script>
     <link rel="stylesheet"
         href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/styles/github-dark.min.css">
+    <script>
+        function toggleTheme() {
+            const html = document.documentElement;
+            const current = html.getAttribute('data-theme');
+            const next = current === 'dark' ? 'light' : (current === 'light' ? 'dark' :
+                (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'light' : 'dark'));
+            html.setAttribute('data-theme', next);
+            localStorage.setItem('theme', next);
+        }
+        (function () {
+            const saved = localStorage.getItem('theme');
+            if (saved) document.documentElement.setAttribute('data-theme', saved);
+        })();
+        document.querySelectorAll('pre code').forEach(el => hljs.highlightElement(el));
+        document.querySelectorAll('.markdown-body').forEach(el => {
+            el.innerHTML = marked.parse(el.textContent);
+            el.classList.add('rendered-md');
+            el.querySelectorAll('pre code').forEach(c => hljs.highlightElement(c));
+        });
+    </script>
 </body>
 
 </html>