package gistatic
import (
"flag"
"fmt"
"html/template"
"io"
"os"
"os/exec"
"path"
"slices"
"strings"
g "gobang"
gt "gotext"
)
const (
dateFmt = "--date=format:%Y-%m-%dT%H:%M"
)
var (
fn_wd = os.Getwd
fn_mkdirp = func(path string) error {
return os.MkdirAll(path, 0777)
}
fn_writeFile = func(path string, content string) error {
return os.WriteFile(path, []byte(content), 0644)
}
logoSVGUncolored = g.Heredoc(`
`)
styleCSS = g.Heredoc(`
:root {
--color: black;
--background-color: white;
--background-contrast-color: hsl(0, 0%, 98%);
--hover-color: hsl(0, 0%, 93%);
--nav-color: hsl(0, 0%, 87%);
--selected-color: hsl(0, 0%, 80%);
--diff-added-color: hsl(120, 100%, 23%);
--diff-removed-color: hsl(0, 100%, 47%);
}
@media(prefers-color-scheme: dark) {
:root {
--color: white;
--background-color: black;
--background-contrast-color: hsl(0, 0%, 2%);
--hover-color: hsl(0, 0%, 7%);
--nav-color: hsl(0, 0%, 13%);
--selected-color: hsl(0, 0%, 20%);
}
body {
color: var(--color);
background-color: var(--background-color);
}
a {
color: hsl(211, 100%, 60%);
}
a:visited {
color: hsl(242, 100%, 80%);
}
}
body {
font-family: monospace;
max-width: 1100px;
margin: 0 auto 0 auto;
}
.logo {
height: 6em;
width: 6em;
}
.header-horizontal-grouping {
display: flex;
align-items: center;
margin-top: 1em;
margin-bottom: 1em;
}
.header-description {
margin-left: 2em;
}
nav {
margin-top: 2em;
}
nav ul {
display: flex;
list-style-type: none;
margin-bottom: 0;
}
nav li {
margin-left: 10px;
}
nav a, nav a:visited {
padding: 2px 8px 0px 8px;
color: var(--color);
}
.selected-nav-item {
background-color: var(--nav-color);
}
hr {
margin-top: 0;
border: 0;
border-top: 3px solid var(--nav-color);
}
table {
margin: 2em auto;
}
th {
padding-bottom: 1em;
}
tbody tr:hover {
background-color: var(--hover-color);
}
td {
padding-left: 1em;
padding-right: 1em;
}
/* commit page */
.diff-added, .diff-removed {
text-decoration: none;
}
.diff-added:target, .diff-removed:target {
background-color: var(--selected-color);
}
.diff-added, .diff-added:visited {
color: var(--diff-added-color);
}
.diff-removed, .diff-removed:visited {
color: var(--diff-removed-color);
}
/* log page */
.log-commit-box {
padding: 1em;
margin: 1em;
background-color: var(--background-contrast-color);
}
.log-commit-tag {
padding: 2px;
border: 1px solid;
color: var(--color);
}
.log-head-highlight {
background-color: #ff8888; /* FIXME: hsl + dark-mode */
}
.log-branch-highlight {
background-color: #88ff88; /* FIXME: hsl + dark-mode */
}
.log-tag-highlight {
background-color: #ffff88; /* FIXME: hsl + dark-mode */
}
.pre-wrapping {
overflow: auto;
margin: 1em;
}
.log-notes {
/* FIXME: yellow box goes until the end of the screen */
padding: 1em;
background-color: #ffd; /* FIXME: hsl + dark-mode */
}
.log-pagination {
text-align: center;
margin: 2em;
}
footer {
text-align: center;
}
`)
listingHTML = g.Must(template.New("").Parse(g.Heredoc(`
{{.title}}
{{.name}}
|
{{.description}}
|
{{.lastCommit}}
|
{{range .repositories}}
{{.name}}
|
{{.description}}
|
|
{{end}}
`)))
repoindexHTML = g.Must(template.New("").Parse(g.Heredoc(`
{{.title}}
{{.name}}
|
{{.description}}
|
{{.lastCommit}}
|
{{range .Repositories}}
{{.name}}
|
{{.description}}
|
|
{{end}}
`)))
branchesHTML = g.Must(template.New("").Parse(g.Heredoc(`
{{.branches}} ยท {{.metadata.name}}
`)))
tagsHTML = g.Must(template.New("").Parse(g.Heredoc(`
{{.title}}
{{.name}}
|
{{.description}}
|
{{.lastCommit}}
|
{{range .Repositories}}
{{.name}}
|
{{.description}}
|
|
{{end}}
`)))
logHTML = g.Must(template.New("").Parse(g.Heredoc(`
{{.title}}
{{.name}}
|
{{.description}}
|
{{.lastCommit}}
|
{{range .Repositories}}
{{.name}}
|
{{.description}}
|
|
{{end}}
`)))
commitHTML = g.Must(template.New("").Parse(g.Heredoc(`
{{.title}}
{{.name}}
|
{{.description}}
|
{{.lastCommit}}
|
{{range .Repositories}}
{{.name}}
|
{{.description}}
|
|
{{end}}
`)))
sourceHTML = g.Must(template.New("").Parse(g.Heredoc(`
{{.title}}
{{.name}}
|
{{.description}}
|
{{.lastCommit}}
|
{{range .repositories}}
{{.name}}
|
{{.description}}
|
|
{{end}}
`)))
)
type repositoryTemplateT struct{
template *template.Template
path string
keys map[string]interface{}
}
type argsT struct{
allArgs []string
subArgs []string
cloneURL string
outdir string
}
func logoSVG(color string) string {
return strings.ReplaceAll(logoSVGUncolored, "$color", color)
}
func generateAssets(outdir string) error {
err := fn_mkdirp(outdir + "/img/logo")
if err != nil {
return err
}
err = fn_writeFile(outdir + "/img/favicon.svg", logoSVG("black"))
if err != nil {
return err
}
err = fn_writeFile(outdir + "/img/logo/light.svg", logoSVG("black"))
if err != nil {
return err
}
err = fn_writeFile(outdir + "/img/logo/dark.svg", logoSVG("white"))
if err != nil {
return err
}
err = fn_writeFile(outdir + "/style.css", styleCSS)
if err != nil {
return err
}
return nil
}
func writePage(
dir string,
t *template.Template,
filename string,
keys map[string]interface{},
) {
fullpath := dir + "/" + filename
s := strings.Builder{}
g.PanicIf(fn_mkdirp(path.Dir(fullpath)))
g.PanicIf(t.Execute(&s, keys))
g.PanicIf(fn_writeFile(fullpath, s.String()))
}
func cmd(stderr io.Writer, name string, args ...string) (string, error) {
command := exec.Command(name, args...)
command.Stderr = stderr
outBytes, err := command.Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(outBytes)), nil
}
func branchesData(
gitcmd func(...string) (string, error),
) ([]map[string]interface{}, error) {
branches := []map[string]interface{}{}
names, err := gitcmd("branch", "--format", "%(refname:lstrip=2)")
if err != nil {
return nil, err
}
for _, name := range strings.Split(names, "\n") {
sha, err := gitcmd("rev-parse", name)
if err != nil {
return nil, err
}
messageSummary, err := gitcmd("log", "-1", "--format=%s", sha)
if err != nil {
return nil, err
}
author, err := gitcmd("log", "-1", "--format=%an", sha)
if err != nil {
return nil, err
}
date, err := gitcmd(
"log", "-1", "--format=%cd", dateFmt, sha,
)
if err != nil {
return nil, err
}
branches = append(branches, map[string]interface{}{
"name": name,
"sha": sha,
"messageSummary": messageSummary,
"author": author,
"date": date,
})
}
return branches, nil
}
func generateRepository(
args argsT,
path string,
stderr io.Writer,
metadata map[string]interface{},
) error {
gitcmd := func(args ...string) (string, error) {
return cmd(
stderr,
"git",
slices.Concat(
[]string{"-C", path},
args,
)...,
)
}
branches, err := branchesData(gitcmd)
if err != nil {
return err
}
dir := args.outdir + "/" + (metadata["name"].(string))
keys := map[string]interface{}{
"metadata": metadata,
"cloneURL": args.cloneURL,
"logoAlt": gt.Gettext(
"Outlined icon of 3 melting ice cubes",
),
"language": gt.Gettext("en"),
"branches": gt.Gettext("branches"),
"tags": gt.Gettext("tags"),
"log": gt.Gettext("log"),
"commit": gt.Gettext("commit"),
"branch": gt.Gettext("Branch"),
"tag": gt.Gettext("Tag"),
"commitMessage": gt.Gettext("Commit message"),
"author": gt.Gettext("Author"),
"date": gt.Gettext("Date"),
"data": map[string]interface{}{
"branches": branches,
},
}
writePage(dir, repoindexHTML, "index.html", keys)
writePage(dir, branchesHTML, "branches.html", keys)
// commits := allCommits()
return nil
}
func basicRepositoryMetadata(path string) (map[string]interface{}, error) {
gitdirBytes, err := exec.Command(
"git", "-C", path, "rev-parse", "--git-dir",
).Output()
if err != nil {
return nil, err
}
gitdir := strings.TrimSpace(string(gitdirBytes))
descriptionPath := path + "/" + gitdir + "/" + "description"
descriptionBytes, err := os.ReadFile(descriptionPath)
if err != nil && !os.IsNotExist(err) {
return nil, err
}
description := strings.TrimSpace(string(descriptionBytes))
lastCommitBytes, err := exec.Command(
"git", "-C", path, "log", "-1", "--format=%cd", dateFmt,
).Output()
if err != nil {
return nil, err
}
lastCommit := strings.TrimSpace(string(lastCommitBytes))
return map[string]interface{}{
"name": path,
"description": description,
"lastCommitDate": lastCommit,
}, nil
}
func generateRepositories(
args argsT,
stderr io.Writer,
) ([]map[string]interface{}, error) {
allMetadata := []map[string]interface{}{}
for _, path := range args.subArgs {
metadata, err := basicRepositoryMetadata(path)
if err != nil {
return nil, err
}
err = generateRepository(args, path, stderr, metadata)
if err != nil {
return nil, err
}
allMetadata = append(allMetadata, metadata)
}
return allMetadata, nil
}
func generateListing(args argsT, allMetadata []map[string]interface{}) error {
keys := map[string]interface{}{
"logoAlt": gt.Gettext(
"Outlined icon of 3 melting ice cubes",
),
"language": gt.Gettext("en"),
"pageDescription": gt.Gettext("Listing of repositories"),
"title": gt.Gettext("Repositories"),
"name": gt.Gettext("Name"),
"description": gt.Gettext("Description"),
"lastCommit": gt.Gettext("Last commit"),
"generatedWith": gt.Gettext("Generated with"),
"repositories": allMetadata,
}
s := strings.Builder{}
err := listingHTML.Execute(&s, keys)
if err != nil {
return err
}
err = fn_writeFile(args.outdir + "/index.html", s.String())
if err != nil {
return err
}
return nil
}
func run(args argsT, _ io.Reader, _ io.Writer, stderr io.Writer) int {
err := fn_mkdirp(args.outdir)
if err != nil {
fmt.Fprintln(stderr, err)
return 1
}
err = generateAssets(args.outdir)
if err != nil {
fmt.Fprintln(stderr, err)
return 1
}
allMetadata, err := generateRepositories(args, stderr)
if err != nil {
fmt.Fprintln(stderr, err)
return 1
}
err = generateListing(args, allMetadata)
if err != nil {
fmt.Fprintln(stderr, err)
return 1
}
return 0
}
func usage(argv0 string, w io.Writer) {
fmt.Fprintf(
w,
"Usage: %s [-o DIRECTORY] [-u CLONE_URL] REPOSITORY...\n",
argv0,
)
}
func getopt(allArgs []string, w io.Writer) (argsT, int) {
argv0 := allArgs[0]
argv := allArgs[1:]
fs := flag.NewFlagSet("", flag.ContinueOnError)
fs.Usage = func() {}
fs.SetOutput(w)
outdir := fs.String(
"o",
g.Must(fn_wd()),
"Where to store the generated files",
)
cloneURL := fs.String(
"u",
"",
"The prefix of the online cloning addresss",
)
if fs.Parse(argv) != nil {
usage(argv0, w)
return argsT{}, 2
}
subArgs := fs.Args()
return argsT{
allArgs: allArgs,
subArgs: subArgs,
cloneURL: *cloneURL,
outdir: *outdir,
}, 0
}
func Main() {
g.Init()
gt.Init(Name, LOCALEDIR)
args, rc := getopt(os.Args, os.Stderr)
g.ExitIf(rc)
os.Exit(run(args, os.Stdin, os.Stdout, os.Stderr))
}