Adds files.html and repo structure

This commit is contained in:
Miguel de la Cruz 2024-06-30 00:25:22 +02:00
parent 28f94c1499
commit 809ab91837
5 changed files with 289 additions and 42 deletions

View file

@ -9,10 +9,12 @@ Project heavily inspired by the amazing
- [X] Embed templates mechanism. - [X] Embed templates mechanism.
- [ ] Embed style.css - [ ] Embed style.css
- [ ] Correctly manage relative links.
- [ ] Add a base url to avoid relative links if provided.
- [X] Generate the index html file for the `index` subcommand. - [X] Generate the index html file for the `index` subcommand.
- [ ] Generate the log html file for a repository. - [ ] Generate the log html file for a repository.
- [ ] Detect and link README, LICENSE and CONTRIBUTING files. - [ ] Detect and link README, LICENSE and CONTRIBUTING files.
- [ ] Generate the files html file and file structure. - [X] Generate the files html file and file structure.
- [ ] Generate the refs html file. - [ ] Generate the refs html file.
- [X] Add a proper CLI parsing and subcommands. - [X] Add a proper CLI parsing and subcommands.
- [ ] Add a sample CSS file for the default templates. - [ ] Add a sample CSS file for the default templates.
@ -20,9 +22,10 @@ Project heavily inspired by the amazing
modified. modified.
- [ ] Take binary files into account. - [ ] Take binary files into account.
- [ ] Limit the output for large diffs. - [ ] Limit the output for large diffs.
- [ ] Allow to anchor lines. - [X] Allow to anchor lines.
- [ ] Check if the templates exist on a location and use them if - [ ] Check if the templates exist on a location and use them if
so. Allow to change that location through CLI flags or env vars. so. Allow to change that location through CLI flags or env vars.
- [ ] Optimize tree generation, it is currently very time consuming.
- [ ] Optimize repository generation through a cache. - [ ] Optimize repository generation through a cache.
- [ ] Add a flag to regenerate in case a `push -f` comes in. - [ ] Add a flag to regenerate in case a `push -f` comes in.
- [ ] Optimize output generation through the use of smaller templates - [ ] Optimize output generation through the use of smaller templates

8
cmd.go
View file

@ -4,10 +4,18 @@ type indexCmd struct {
Paths []string `arg:"" help:"The paths to the repositories to include in the index."` Paths []string `arg:"" help:"The paths to the repositories to include in the index."`
} }
func (i *indexCmd) Run() error {
return generateIndex(i.Paths)
}
type repoCmd struct { type repoCmd struct {
Path string `arg:"" help:"The path to the repository."` Path string `arg:"" help:"The path to the repository."`
} }
func (r *repoCmd) Run() error {
return generateRepo(r.Path)
}
var cli struct { var cli struct {
Debug bool `help:"Print debug information."` Debug bool `help:"Print debug information."`

208
gitssg.go
View file

@ -3,7 +3,6 @@ package main
import ( import (
"bytes" "bytes"
"embed" "embed"
"flag"
"fmt" "fmt"
"html/template" "html/template"
"log/slog" "log/slog"
@ -14,6 +13,7 @@ import (
"github.com/alecthomas/kong" "github.com/alecthomas/kong"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
) )
type RepoDir struct { type RepoDir struct {
@ -23,56 +23,46 @@ type RepoDir struct {
LastCommit time.Time LastCommit time.Time
} }
type RepoInfo struct {
Name string
Description string
Url string
HasReadme bool
HasLicense bool
HasContributing bool
}
type CommitFile struct {
Mode string
Path string
Name string
Lines int
}
//go:embed templates //go:embed templates
var embedTmpl embed.FS var embedTmpl embed.FS
var funcMap = template.FuncMap{"inc": func(i int) int { return i + 1 }}
func executeTemplate(name string, data any) (string, error) { // ToDo: add a map function to generate the menu
func executeTemplate(name string, data any) ([]byte, error) {
path := filepath.Join("templates", fmt.Sprintf("%s.html.tmpl", name)) path := filepath.Join("templates", fmt.Sprintf("%s.html.tmpl", name))
b, err := embedTmpl.ReadFile(path) b, err := embedTmpl.ReadFile(path)
if err != nil { if err != nil {
return "", fmt.Errorf("cannot read embedded file %q: %w", path, err) return nil, fmt.Errorf("cannot read embedded file %q: %w", path, err)
} }
tmpl, err := template.New("").Parse(string(b)) tmpl, err := template.New("").Funcs(funcMap).Parse(string(b))
if err != nil { if err != nil {
return "", fmt.Errorf("cannot parse template %q: %w", path, err) return nil, fmt.Errorf("cannot parse template %q: %w", path, err)
} }
var strb bytes.Buffer var strb bytes.Buffer
if err := tmpl.Execute(&strb, data); err != nil { if err := tmpl.Execute(&strb, data); err != nil {
return "", fmt.Errorf("cannot execute template %q: %w", path, err) return nil, fmt.Errorf("cannot execute template %q: %w", path, err)
} }
return strb.String(), nil return strb.Bytes(), nil
}
func errAndExit(msg string, args ...any) {
fmt.Fprintf(os.Stderr, msg, args...)
os.Exit(1)
}
func (i *indexCmd) Run() error {
slog.Debug("Generating index", "args", flag.Args())
return generateIndex(i.Paths)
}
func (r *repoCmd) Run() error {
slog.Debug("Generating repository", "path", r.Path)
return generateRepo(r.Path)
}
func main() {
ctx := kong.Parse(&cli)
if cli.Debug {
slog.SetLogLoggerLevel(slog.LevelDebug)
}
if err := ctx.Run(); err != nil {
fmt.Fprintf(os.Stderr, "error: %s\n", err)
os.Exit(1)
}
} }
func readFile(path string) (string, error) { func readFile(path string) (string, error) {
@ -101,18 +91,22 @@ func readRepoFile(dirname, name string) (contents string, err error) {
} }
func generateIndex(args []string) error { func generateIndex(args []string) error {
slog.Debug("Generating index", "args", args)
repoDirs := []*RepoDir{} repoDirs := []*RepoDir{}
for _, dirname := range args { for _, dirname := range args {
slog.Debug("Processing directory", "dirname", dirname) slog.Debug("Processing directory", "dirname", dirname)
reponame := strings.TrimSuffix(filepath.Base(dirname), ".git") reponame := strings.TrimSuffix(filepath.Base(dirname), ".git")
repoDir := &RepoDir{Name: reponame} repoDir := &RepoDir{Name: reponame}
// read description
description, err := readRepoFile(dirname, "description") description, err := readRepoFile(dirname, "description")
if err != nil { if err != nil {
return fmt.Errorf("cannot read description for repository %q: %w", dirname, err) return fmt.Errorf("cannot read description for repository %q: %w", dirname, err)
} }
repoDir.Description = description repoDir.Description = description
// read owner
owner, err := readRepoFile(dirname, "owner") owner, err := readRepoFile(dirname, "owner")
if err != nil { if err != nil {
return fmt.Errorf("cannot read owner for repository %q: %w", dirname, err) return fmt.Errorf("cannot read owner for repository %q: %w", dirname, err)
@ -139,15 +133,13 @@ func generateIndex(args []string) error {
repoDirs = append(repoDirs, repoDir) repoDirs = append(repoDirs, repoDir)
} }
data := map[string]any{ data := map[string]any{"repoDirs": repoDirs}
"repoDirs": repoDirs,
}
contents, err := executeTemplate("index", data) contents, err := executeTemplate("index", data)
if err != nil { if err != nil {
return fmt.Errorf("cannot execute index template: %w", err) return fmt.Errorf("cannot execute index template: %w", err)
} }
if err := os.WriteFile("index.html", []byte(contents), 0755); err != nil { if err := os.WriteFile("index.html", contents, 0755); err != nil {
return fmt.Errorf("cannot write index contents to \"index.html\": %w", err) return fmt.Errorf("cannot write index contents to \"index.html\": %w", err)
} }
@ -155,5 +147,141 @@ func generateIndex(args []string) error {
} }
func generateRepo(path string) error { func generateRepo(path string) error {
slog.Debug("Generating repository", "path", path)
repoInfo := &RepoInfo{Name: strings.TrimSuffix(filepath.Base(path), ".git")}
// create and clean destination directory
fi, err := os.Stat(repoInfo.Name)
if err == nil && !fi.IsDir() {
return fmt.Errorf("path %q is not a directory", repoInfo.Name)
}
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("cannot stat %q: %w", repoInfo.Name, err)
}
if err == nil && fi.IsDir() {
if err := os.RemoveAll(repoInfo.Name); err != nil {
return fmt.Errorf("cannot delete directory for %q: %w", repoInfo.Name, err)
}
}
if err := os.Mkdir(repoInfo.Name, 0755); err != nil {
return fmt.Errorf("cannot create directory for %q: %w", repoInfo.Name, err)
}
// read description
description, err := readRepoFile(path, "description")
if err != nil {
return fmt.Errorf("cannot read description for repository %q: %w", path, err)
}
repoInfo.Description = description
// read url
url, err := readRepoFile(path, "url")
if err != nil {
return fmt.Errorf("cannot read url for repository %q: %w", path, err)
}
repoInfo.Url = url
// open repository
repo, err := git.PlainOpen(path)
if err != nil {
return fmt.Errorf("cannot open repository %q: %w", path, err)
}
head, err := repo.Head()
if err != nil {
return fmt.Errorf("cannot get repo head: %w", err)
}
c, err := repo.CommitObject(head.Hash())
if err != nil {
return fmt.Errorf("cannot get commit for hash %s: %w", head.Hash(), err)
}
// ToDo: populate hasReadme, hasLicense and hasContributing
tree, err := c.Tree()
if err != nil {
return fmt.Errorf("cannot get tree for commit %s: %w", c, err)
}
slog.Debug("Processing repository tree", "head", head.Hash())
files := []*CommitFile{}
iErr := tree.Files().ForEach(func(o *object.File) error {
slog.Debug("Processing tree file", "name", o.Name)
dirpath := filepath.Join(repoInfo.Name, "file", filepath.Dir(o.Name))
filename := filepath.Base(o.Name)
slog.Debug("Creating directory", "dirpath", dirpath)
if err := os.MkdirAll(dirpath, 0755); err != nil {
return fmt.Errorf("cannot create directory %q: %w", dirpath, err)
}
data := map[string]any{
"repoInfo": repoInfo,
"filename": filename,
"filesize": o.Size,
}
isBinary, err := o.IsBinary()
if err != nil {
return fmt.Errorf("cannot get binary state for file %q: %w", o.Name, err)
}
data["isBinary"] = isBinary
if !isBinary {
lines, err := o.Lines()
if err != nil {
return fmt.Errorf("cannot get lines for %q: %w", o.Name, err)
}
data["filelines"] = lines
}
contents, err := executeTemplate("file", data)
if err != nil {
return fmt.Errorf("cannot execute file template for file %q: %w", o.Name, err)
}
dstpath := filepath.Join(dirpath, fmt.Sprintf("%s.html", filename))
if err := os.WriteFile(dstpath, contents, 0755); err != nil {
return fmt.Errorf("cannot write file contents to %q: %w", dstpath, err)
}
file := &CommitFile{
Mode: o.Mode.String(), // ToDo: correctly calculate the mode string
Path: filepath.Join("file", fmt.Sprintf("%s.html", o.Name)),
Name: o.Name,
}
files = append(files, file)
return nil
})
if iErr != nil {
return fmt.Errorf("error while processing tree: %w", iErr)
}
// ToDo: bundle execute and write into a function
data := map[string]any{"repoInfo": repoInfo, "files": files}
contents, err := executeTemplate("files", data)
if err != nil {
return fmt.Errorf("cannot execute files template for repository %q: %w", repoInfo.Name, err)
}
if err := os.WriteFile(filepath.Join(repoInfo.Name, "files.html"), contents, 0755); err != nil {
return fmt.Errorf("cannot write files contents to \"files.html\": %w", err)
}
return nil return nil
} }
func main() {
ctx := kong.Parse(&cli)
if cli.Debug {
slog.SetLogLoggerLevel(slog.LevelDebug)
}
if err := ctx.Run(); err != nil {
fmt.Fprintf(os.Stderr, "error: %s\n", err)
os.Exit(1)
}
}

53
templates/file.html.tmpl Normal file
View file

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{.filename}} - {{.repoInfo.Name}} - {{.repoInfo.Description}}</title>
<link rel="icon" type="image/png" href="../favicon.png" />
<link rel="alternate" type="application/atom+xml" title="bag Atom Feed" href="../atom.xml" />
<link rel="alternate" type="application/atom+xml" title="bag Atom Feed (tags)" href="../tags.xml" />
<link rel="stylesheet" type="text/css" href="../style.css" />
</head>
<body>
<table>
<tr>
<td><a href="../../"><img src="../logo.png" alt="" width="32" height="32" /></a></td>
<td><h1>{{.repoInfo.Name}}</h1><span class="desc">{{.repoInfo.Description}}</span></td>
</tr>
<tr class="url">
<td></td>
<td>
{{if ne .repoInfo.Url ""}}
git clone <a href="{{.repoInfo.Url}}">{{.repoInfo.Url}}</a>
{{end}}
</td>
</tr>
<tr>
<td></td>
<td>
<a href="../log.html">Log</a>
| <a href="../files.html">Files</a>
| <a href="../refs.html">Refs</a>
{{if .repoInfo.HasReadme}}
| <a href="../file/README.html">README</a>
{{end}}
{{if .repoInfo.HasLicense}}
| <a href="../file/LICENSE.html">LICENSE</a></td>
{{end}}
{{if .repoInfo.HasContributing}}
| <a href="../file/CONTRIBUTING.html">CONTRIBUTING</a></td>
{{end}}
</tr>
</table>
<hr/>
<div id="content">
<p> {{.filename}} ({{.filesize}}B)</p><hr/><pre id="blob">
{{- range $i, $line := .filelines}}
{{- $index := inc $i}}
<a href="#l{{$index}}" class="line" id="l{{$index}}">{{printf "%7d" $index}}</a> {{$line}}
{{- end}}
</pre>
</div>
</body>
</html>

55
templates/files.html.tmpl Normal file
View file

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Files - {{.repoInfo.Name}} - {{.repoInfo.Description}}
</title>
<link rel="icon" type="image/png" href="favicon.png" />
<link rel="alternate" type="application/atom+xml" title="stagit Atom Feed" href="atom.xml" />
<link rel="alternate" type="application/atom+xml" title="stagit Atom Feed (tags)" href="tags.xml" />
<link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
<table>
<tr>
<td><a href="../"><img src="logo.png" alt="" width="32" height="32" /></a></td>
<td><h1>stagit</h1><span class="desc">static git page generator</span></td>
</tr>
<tr class="url">
<td></td>
<td>git clone <a href="git://git.codemadness.org/stagit">git://git.codemadness.org/stagit</a></td>
</tr>
<tr>
<td></td>
<td>
<a href="log.html">Log</a>
| <a href="files.html">Files</a>
| <a href="refs.html">Refs</a>
| <a href="file/README.html">README</a>
| <a href="file/LICENSE.html">LICENSE</a>
</td>
</tr>
</table>
<hr/>
<div id="content">
<table id="files"><thead>
<tr>
<td><b>Mode</b></td>
<td><b>Name</b></td>
<td class="num" align="right"><b>Size</b></td>
</tr>
</thead>
<tbody>
{{range .files}}
<tr>
<td>{{.Mode}}</td>
<td><a href="{{.Path}}">{{.Name}}</a></td>
<td class="num" align="right">{{.Lines}}L</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</body>
</html>