Commit b446157
Changed files (9)
internal
generator
templates
internal/generator/index.go
@@ -1,130 +0,0 @@
-package generator
-
-import (
- "os"
- "path/filepath"
- "sort"
- "strings"
-
- "mokhan.ca/antonmedv/gitmal/internal/git"
- "mokhan.ca/antonmedv/gitmal/internal/links"
- "mokhan.ca/antonmedv/gitmal/internal/templates"
-)
-
-func GenerateIndex(files []git.Blob, params Params) error {
- // Build directory indexes
- type dirInfo struct {
- subdirs map[string]struct{}
- files []git.Blob
- }
- dirs := map[string]*dirInfo{}
-
- ensureDir := func(p string) *dirInfo {
- if di, ok := dirs[p]; ok {
- return di
- }
- di := &dirInfo{subdirs: map[string]struct{}{}, files: []git.Blob{}}
- dirs[p] = di
- return di
- }
-
- dirsSet := links.BuildDirSet(files)
- filesSet := links.BuildFileSet(files)
-
- for _, b := range files {
- // Normalize to forward slash paths for URL construction
- p := b.Path
- parts := strings.Split(p, "/")
- // walk directories
- cur := ""
- for i := 0; i < len(parts)-1; i++ {
- child := parts[i]
- ensureDir(cur).subdirs[child] = struct{}{}
- if cur == "" {
- cur = child
- } else {
- cur = cur + "/" + child
- }
- ensureDir(cur) // ensure it exists
- }
- ensureDir(cur).files = append(ensureDir(cur).files, b)
- }
-
- di := dirs[""] // root
-
- outDir := params.OutputDir
- if err := os.MkdirAll(outDir, 0o755); err != nil {
- return err
- }
-
- // Build entries
- dirNames := make([]string, 0, len(di.subdirs))
- for name := range di.subdirs {
- dirNames = append(dirNames, name)
- }
- // Sort for stable output
- sort.Strings(dirNames)
- sort.Slice(di.files, func(i, j int) bool {
- return di.files[i].FileName < di.files[j].FileName
- })
-
- subdirEntries := make([]templates.ListEntry, 0, len(dirNames))
- for _, name := range dirNames {
- subdirEntries = append(subdirEntries, templates.ListEntry{
- Name: name + "/",
- Href: "blob/" + params.Ref.DirName() + "/" + name + "/index.html",
- IsDir: true,
- })
- }
-
- fileEntries := make([]templates.ListEntry, 0, len(di.files))
- for _, b := range di.files {
- fileEntries = append(fileEntries, templates.ListEntry{
- Name: b.FileName + "",
- Href: "blob/" + params.Ref.DirName() + "/" + b.FileName + ".html",
- Mode: b.Mode,
- Size: humanizeSize(b.Size),
- })
- }
-
- // Title and current path label
- title := params.Name
-
- f, err := os.Create(filepath.Join(outDir, "index.html"))
- if err != nil {
- return err
- }
-
- rootHref := "./"
- readmeHTML := readme(di.files, dirsSet, filesSet, params, rootHref)
- hasReadme := readmeHTML != ""
-
- err = templates.ListTemplate.ExecuteTemplate(f, "layout.gohtml", templates.ListParams{
- LayoutParams: templates.LayoutParams{
- Title: title,
- Name: params.Name,
- RootHref: rootHref,
- CurrentRefDir: params.Ref.DirName(),
- Selected: "code",
- NeedsMarkdownCSS: hasReadme,
- NeedsSyntaxCSS: hasReadme,
- },
- HeaderParams: templates.HeaderParams{
- Ref: params.Ref,
- Breadcrumbs: breadcrumbs(params.Name, "", false),
- },
- Ref: params.Ref,
- Dirs: subdirEntries,
- Files: fileEntries,
- Readme: readmeHTML,
- })
- if err != nil {
- _ = f.Close()
- return err
- }
- if err := f.Close(); err != nil {
- return err
- }
-
- return nil
-}
internal/generator/list.go
@@ -13,12 +13,12 @@ import (
"mokhan.ca/antonmedv/gitmal/internal/templates"
)
-func GenerateLists(files []git.Blob, params Params) error {
- // Build directory indexes
- type dirInfo struct {
- subdirs map[string]struct{}
- files []git.Blob
- }
+type dirInfo struct {
+ subdirs map[string]struct{}
+ files []git.Blob
+}
+
+func buildDirTree(files []git.Blob) map[string]*dirInfo {
dirs := map[string]*dirInfo{}
ensureDir := func(p string) *dirInfo {
@@ -30,14 +30,9 @@ func GenerateLists(files []git.Blob, params Params) error {
return di
}
- dirsSet := links.BuildDirSet(files)
- filesSet := links.BuildFileSet(files)
-
for _, b := range files {
- // Normalize to forward slash paths for URL construction
p := b.Path
parts := strings.Split(p, "/")
- // walk directories
cur := ""
for i := 0; i < len(parts)-1; i++ {
child := parts[i]
@@ -47,11 +42,96 @@ func GenerateLists(files []git.Blob, params Params) error {
} else {
cur = cur + "/" + child
}
- ensureDir(cur) // ensure it exists
+ ensureDir(cur)
}
ensureDir(cur).files = append(ensureDir(cur).files, b)
}
+ return dirs
+}
+
+func GenerateIndex(files []git.Blob, params Params) error {
+ dirs := buildDirTree(files)
+ dirsSet := links.BuildDirSet(files)
+ filesSet := links.BuildFileSet(files)
+
+ di := dirs[""]
+
+ outDir := params.OutputDir
+ if err := os.MkdirAll(outDir, 0o755); err != nil {
+ return err
+ }
+
+ dirNames := make([]string, 0, len(di.subdirs))
+ for name := range di.subdirs {
+ dirNames = append(dirNames, name)
+ }
+ sort.Strings(dirNames)
+ sort.Slice(di.files, func(i, j int) bool {
+ return di.files[i].FileName < di.files[j].FileName
+ })
+
+ subdirEntries := make([]templates.ListEntry, 0, len(dirNames))
+ for _, name := range dirNames {
+ subdirEntries = append(subdirEntries, templates.ListEntry{
+ Name: name + "/",
+ Href: "blob/" + params.Ref.DirName() + "/" + name + "/index.html",
+ IsDir: true,
+ })
+ }
+
+ fileEntries := make([]templates.ListEntry, 0, len(di.files))
+ for _, b := range di.files {
+ fileEntries = append(fileEntries, templates.ListEntry{
+ Name: b.FileName,
+ Href: "blob/" + params.Ref.DirName() + "/" + b.FileName + ".html",
+ Mode: b.Mode,
+ Size: humanizeSize(b.Size),
+ })
+ }
+
+ title := params.Name
+
+ f, err := os.Create(filepath.Join(outDir, "index.html"))
+ if err != nil {
+ return err
+ }
+
+ rootHref := "./"
+ readmeHTML := readme(di.files, dirsSet, filesSet, params, rootHref)
+ hasReadme := readmeHTML != ""
+
+ err = templates.ListTemplate.ExecuteTemplate(f, "layout.gohtml", templates.ListParams{
+ LayoutParams: templates.LayoutParams{
+ Title: title,
+ Name: params.Name,
+ RootHref: rootHref,
+ CurrentRefDir: params.Ref.DirName(),
+ Selected: "code",
+ NeedsMarkdownCSS: hasReadme,
+ NeedsSyntaxCSS: hasReadme,
+ },
+ HeaderParams: templates.HeaderParams{
+ Ref: params.Ref,
+ Breadcrumbs: breadcrumbs(params.Name, "", false),
+ },
+ Ref: params.Ref,
+ Dirs: subdirEntries,
+ Files: fileEntries,
+ Readme: readmeHTML,
+ })
+ if err != nil {
+ _ = f.Close()
+ return err
+ }
+ return f.Close()
+}
+
+func GenerateLists(files []git.Blob, params Params) error {
+ dirs := buildDirTree(files)
+ dirsSet := links.BuildDirSet(files)
+ filesSet := links.BuildFileSet(files)
+
type job struct {
dirPath string
di *dirInfo
@@ -96,7 +176,7 @@ func GenerateLists(files []git.Blob, params Params) error {
fileEntries := make([]templates.ListEntry, 0, len(di.files))
for _, b := range di.files {
fileEntries = append(fileEntries, templates.ListEntry{
- Name: b.FileName + "",
+ Name: b.FileName,
Href: b.FileName + ".html",
Mode: b.Mode,
Size: humanizeSize(b.Size),
internal/templates/css/layout.css
@@ -174,26 +174,6 @@ footer {
text-align: center;
}
-.header-container {
- container-type: scroll-state;
- position: sticky;
- top: 0;
-}
-
-.header-container {
- @container scroll-state(stuck: top) {
- header {
- border-top: none;
- border-top-left-radius: 0;
- border-top-right-radius: 0;
- }
-
- .goto-top {
- display: flex;
- }
- }
-}
-
header {
display: flex;
flex-direction: row;
@@ -238,20 +218,3 @@ header .path {
.breadcrumbs a {
word-break: break-all;
}
-
-.goto-top {
- display: none;
- margin-left: auto;
- padding: 6px 10px;
- background: none;
- border: none;
- border-radius: 6px;
- gap: 4px;
- align-items: center;
- color: var(--c-text-1);
- cursor: pointer;
-}
-
-.goto-top:hover {
- background: var(--c-bg-elv);
-}
internal/templates/commit.gohtml
@@ -281,21 +281,13 @@
<div class="files">
{{ range .FileViews }}
<section id="{{ .Path }}" class="file-section">
- <div class="header-container">
- <header class="file-header">
- {{ if .IsRename }}
- <div class="path">{{ .OldName }} → {{ .NewName }}</div>
- {{ else }}
- <div class="path">{{ .Path }}</div>
- {{ end }}
- <button class="goto-top" onclick="window.scrollTo({top: 0, behavior: 'smooth'})">
- <svg aria-hidden="true" focusable="false" width="16" height="16">
- <use xlink:href="#arrow-top"></use>
- </svg>
- Top
- </button>
- </header>
- </div>
+ <header class="file-header">
+ {{ if .IsRename }}
+ <div class="path">{{ .OldName }} → {{ .NewName }}</div>
+ {{ else }}
+ <div class="path">{{ .Path }}</div>
+ {{ end }}
+ </header>
{{ if .IsBinary }}
<div class="binary-file border">Binary file</div>
{{ else if (and .IsRename (not .HasChanges)) }}
internal/templates/header.gohtml
@@ -1,35 +1,27 @@
{{- /*gotype: mokhan.ca/antonmedv/gitmal/pkg/templates.HeaderParams*/ -}}
{{ define "header" }}
- <div class="header-container">
- <header>
- {{ if .Ref }}
- <div class="header-ref" aria-label="Ref">{{ .Ref }}</div>
- {{ end }}
- {{ if .Header }}
- <h1 class="title">{{ .Header }}</h1>
- {{ else if .Breadcrumbs }}
- <nav class="breadcrumbs" aria-label="Breadcrumbs">
- {{ range $i, $b := .Breadcrumbs }}
- {{ if gt $i 0 }}
+ <header>
+ {{ if .Ref }}
+ <div class="header-ref" aria-label="Ref">{{ .Ref }}</div>
+ {{ end }}
+ {{ if .Header }}
+ <h1 class="title">{{ .Header }}</h1>
+ {{ else if .Breadcrumbs }}
+ <nav class="breadcrumbs" aria-label="Breadcrumbs">
+ {{ range $i, $b := .Breadcrumbs }}
+ {{ if gt $i 0 }}
+ <div>/</div>
+ {{ end }}
+ {{ if $b.Href }}
+ <a href="{{$b.Href}}">{{$b.Name}}</a>
+ {{ else }}
+ <h1>{{$b.Name}}</h1>
+ {{ if $b.IsDir}}
<div>/</div>
- {{ end }}
- {{ if $b.Href }}
- <a href="{{$b.Href}}">{{$b.Name}}</a>
- {{ else }}
- <h1>{{$b.Name}}</h1>
- {{ if $b.IsDir}}
- <div>/</div>
- {{ end}}
- {{ end }}
+ {{ end}}
{{ end }}
- </nav>
- {{ end }}
- <button class="goto-top" onclick="window.scrollTo({top: 0, behavior: 'smooth'})">
- <svg aria-hidden="true" focusable="false" width="16" height="16">
- <use xlink:href="#arrow-top"></use>
- </svg>
- Top
- </button>
- </header>
- </div>
+ {{ end }}
+ </nav>
+ {{ end }}
+ </header>
{{ end }}
go.mod
@@ -1,6 +1,6 @@
module mokhan.ca/antonmedv/gitmal
-go 1.24.0
+go 1.25.1
require (
github.com/alecthomas/chroma/v2 v2.20.0
mise.toml
@@ -0,0 +1,2 @@
+[tools]
+go = "1.25.1"
README.md
@@ -28,6 +28,31 @@ Generates a static site with:
- **Branches & Tags** - Navigation between refs
- **Atom feeds** - For commits, tags, and releases
+## URL Structure
+
+Generated paths follow GitHub repository conventions:
+
+| Path | Description |
+|------|-------------|
+| `/index.html` | Repository root (file listing) |
+| `/blob/<ref>/<path>/index.html` | Directory listing |
+| `/blob/<ref>/<file>.html` | File view (syntax highlighted) |
+| `/commit/<hash>.html` | Commit diff view |
+| `/commits/<ref>/index.html` | Commit history (page 1) |
+| `/commits/<ref>/page-N.html` | Commit history (page N) |
+| `/branches.html` | Branches list |
+| `/tags.html` | Tags list |
+
+### Feeds & API
+
+| Path | Description |
+|------|-------------|
+| `/commits/<ref>.atom` | Commits Atom feed |
+| `/commits/<ref>.json` | Commits JSON |
+| `/tags.atom` | Tags Atom feed |
+| `/releases.atom` | Releases Atom feed |
+| `/branches.json` | Branches JSON |
+
## Building
```bash