Adds files.html and repo structure
This commit is contained in:
parent
28f94c1499
commit
809ab91837
5 changed files with 289 additions and 42 deletions
|
@ -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
8
cmd.go
|
@ -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
208
gitssg.go
|
@ -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
53
templates/file.html.tmpl
Normal 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
55
templates/files.html.tmpl
Normal 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>
|
Loading…
Reference in a new issue