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