aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/gistatic.go1064
1 files changed, 1044 insertions, 20 deletions
diff --git a/src/gistatic.go b/src/gistatic.go
index fdbd2c4..3a56db2 100644
--- a/src/gistatic.go
+++ b/src/gistatic.go
@@ -3,25 +3,1055 @@ 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(`
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
+ <path fill="$color" d="M 0 8 L 1 8 L 1 9 L 0 9 L 0 8 Z" />
+ <path fill="$color" d="M 0 13 L 1 13 L 1 14 L 0 14 L 0 13 Z" />
+ <path fill="$color" d="M 1 8 L 2 8 L 2 9 L 1 9 L 1 8 Z" />
+ <path fill="$color" d="M 1 13 L 2 13 L 2 14 L 1 14 L 1 13 Z" />
+ <path fill="$color" d="M 2 8 L 3 8 L 3 9 L 2 9 L 2 8 Z" />
+ <path fill="$color" d="M 2 13 L 3 13 L 3 14 L 2 14 L 2 13 Z" />
+ <path fill="$color" d="M 3 8 L 4 8 L 4 9 L 3 9 L 3 8 Z" />
+ <path fill="$color" d="M 3 13 L 4 13 L 4 14 L 3 14 L 3 13 Z" />
+ <path fill="$color" d="M 4 7 L 5 7 L 5 8 L 4 8 L 4 7 Z" />
+ <path fill="$color" d="M 4 8 L 5 8 L 5 9 L 4 9 L 4 8 Z" />
+ <path fill="$color" d="M 4 13 L 5 13 L 5 14 L 4 14 L 4 13 Z" />
+ <path fill="$color" d="M 5 6 L 6 6 L 6 7 L 5 7 L 5 6 Z" />
+ <path fill="$color" d="M 5 7 L 6 7 L 6 8 L 5 8 L 5 7 Z" />
+ <path fill="$color" d="M 5 13 L 6 13 L 6 14 L 5 14 L 5 13 Z" />
+ <path fill="$color" d="M 6 5 L 7 5 L 7 6 L 6 6 L 6 5 Z" />
+ <path fill="$color" d="M 6 6 L 7 6 L 7 7 L 6 7 L 6 6 Z" />
+ <path fill="$color" d="M 6 14 L 7 14 L 7 15 L 6 15 L 6 14 Z" />
+ <path fill="$color" d="M 7 1 L 8 1 L 8 2 L 7 2 L 7 1 Z" />
+ <path fill="$color" d="M 7 14 L 8 14 L 8 15 L 7 15 L 7 14 Z" />
+ <path fill="$color" d="M 7 15 L 8 15 L 8 16 L 7 16 L 7 15 Z" />
+ <path fill="$color" d="M 7 2 L 8 2 L 8 3 L 7 3 L 7 2 Z" />
+ <path fill="$color" d="M 7 3 L 8 3 L 8 4 L 7 4 L 7 3 Z" />
+ <path fill="$color" d="M 7 4 L 8 4 L 8 5 L 7 5 L 7 4 Z" />
+ <path fill="$color" d="M 7 5 L 8 5 L 8 6 L 7 6 L 7 5 Z" />
+ <path fill="$color" d="M 8 1 L 9 1 L 9 2 L 8 2 L 8 1 Z" />
+ <path fill="$color" d="M 8 15 L 9 15 L 9 16 L 8 16 L 8 15 Z" />
+ <path fill="$color" d="M 9 1 L 10 1 L 10 2 L 9 2 L 9 1 Z" />
+ <path fill="$color" d="M 9 2 L 10 2 L 10 3 L 9 3 L 9 2 Z" />
+ <path fill="$color" d="M 9 6 L 10 6 L 10 7 L 9 7 L 9 6 Z" />
+ <path fill="$color" d="M 9 15 L 10 15 L 10 16 L 9 16 L 9 15 Z" />
+ <path fill="$color" d="M 10 2 L 11 2 L 11 3 L 10 3 L 10 2 Z" />
+ <path fill="$color" d="M 10 3 L 11 3 L 11 4 L 10 4 L 10 3 Z" />
+ <path fill="$color" d="M 10 4 L 11 4 L 11 5 L 10 5 L 10 4 Z" />
+ <path fill="$color" d="M 10 5 L 11 5 L 11 6 L 10 6 L 10 5 Z" />
+ <path fill="$color" d="M 10 6 L 11 6 L 11 7 L 10 7 L 10 6 Z" />
+ <path fill="$color" d="M 11 6 L 12 6 L 12 7 L 11 7 L 11 6 Z" />
+ <path fill="$color" d="M 11 8 L 12 8 L 12 9 L 11 9 L 11 8 Z" />
+ <path fill="$color" d="M 10 15 L 11 15 L 11 16 L 10 16 L 10 15 Z" />
+ <path fill="$color" d="M 11 10 L 12 10 L 12 11 L 11 11 L 11 10 Z" />
+ <path fill="$color" d="M 11 12 L 12 12 L 12 13 L 11 13 L 11 12 Z" />
+ <path fill="$color" d="M 11 14 L 12 14 L 12 15 L 11 15 L 11 14 Z" />
+ <path fill="$color" d="M 11 15 L 12 15 L 12 16 L 11 16 L 11 15 Z" />
+ <path fill="$color" d="M 12 6 L 13 6 L 13 7 L 12 7 L 12 6 Z" />
+ <path fill="$color" d="M 12 8 L 13 8 L 13 9 L 12 9 L 12 8 Z" />
+ <path fill="$color" d="M 12 10 L 13 10 L 13 11 L 12 11 L 12 10 Z" />
+ <path fill="$color" d="M 12 12 L 13 12 L 13 13 L 12 13 L 12 12 Z" />
+ <path fill="$color" d="M 12 14 L 13 14 L 13 15 L 12 15 L 12 14 Z" />
+ <path fill="$color" d="M 13 6 L 14 6 L 14 7 L 13 7 L 13 6 Z" />
+ <path fill="$color" d="M 13 8 L 14 8 L 14 9 L 13 9 L 13 8 Z" />
+ <path fill="$color" d="M 13 10 L 14 10 L 14 11 L 13 11 L 13 10 Z" />
+ <path fill="$color" d="M 13 12 L 14 12 L 14 13 L 13 13 L 13 12 Z" />
+ <path fill="$color" d="M 13 13 L 14 13 L 14 14 L 13 14 L 13 13 Z" />
+ <path fill="$color" d="M 13 14 L 14 14 L 14 15 L 13 15 L 13 14 Z" />
+ <path fill="$color" d="M 14 7 L 15 7 L 15 8 L 14 8 L 14 7 Z" />
+ <path fill="$color" d="M 14 8 L 15 8 L 15 9 L 14 9 L 14 8 Z" />
+ <path fill="$color" d="M 14 9 L 15 9 L 15 10 L 14 10 L 14 9 Z" />
+ <path fill="$color" d="M 14 10 L 15 10 L 15 11 L 14 11 L 14 10 Z" />
+ <path fill="$color" d="M 14 11 L 15 11 L 15 12 L 14 12 L 14 11 Z" />
+ <path fill="$color" d="M 14 12 L 15 12 L 15 13 L 14 13 L 14 12 Z" />
+ </svg>
+ `)
+
+ 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(`
+ <!DOCTYPE html>
+ <html lang="{{.language}}">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <meta name="description" content="{{.pageDescription}}" />
+ <link rel="icon" type="image/svg+xml" href="img/favicon.svg" />
+ <link rel="stylesheet" type="text/css" href="style.css" />
+ <title>{{.title}}</title>
+ </head>
+ <body>
+ <header>
+ <div class="header-horizontal-grouping">
+ <picture>
+ <source srcset="img/logo/dark.svg" media="(prefers-color-scheme: dark)" />
+ <img src="img/logo/light.svg" alt="{{.logoAlt}}" />
+ </picture>
+ <h1 class="header-description">
+ {{.title}}
+ </h1>
+ </div>
+ <hr />
+ </header>
+ <main>
+ <table>
+ <thead>
+ <tr>
+ <th>
+ {{.name}}
+ </th>
+ <th>
+ {{.description}}
+ </th>
+ <th>
+ {{.lastCommit}}
+ </th>
+ </tr>
+ </thead>
+ <tbody>{{range .repositories}}
+ <tr>
+ <td>
+ <a href="{{.name}}">
+ {{.name}}
+ </a>
+ </td>
+ <td>
+ {{.description}}
+ </td>
+ <td>
+ <time datetime="{{.lastCommitDate}}">{{.lastCommitDate}}</time>
+ </td>
+ </tr>{{end}}
+ </tbody>
+ </table>
+ </main>
+ <footer>
+ <hr />
+ <p>
+ {{.generatedWith}} <a href="https://euandre.org/s/gistatic/">gistatic</a>
+ </p>
+ </footer>
+ </body>
+ </html>
+ `)))
+
+ repoindexHTML = g.Must(template.New("").Parse(g.Heredoc(`
+ <!DOCTYPE html>
+ <html lang="{{.language}}">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <meta name="description" content="{{.pageDescription}}" />
+ <link rel="icon" type="image/svg+xml" href="img/favicon.svg" />
+ <link rel="stylesheet" type="text/css" href="style.css" />
+ <title>{{.title}}</title>
+ </head>
+ <body>
+ <header>
+ <div class="header-horizontal-grouping">
+ <picture>
+ <source srcset="img/logo/dark.svg" media="(prefers-color-scheme: dark)" />
+ <img src="img/logo/light.svg" alt="{{.logoAlt}}" />
+ </picture>
+ <h1 class="header-description">
+ {{.title}}
+ </h1>
+ </div>
+ <hr />
+ </header>
+ <main>
+ <table>
+ <thead>
+ <tr>
+ <th>
+ {{.name}}
+ </th>
+ <th>
+ {{.description}}
+ </th>
+ <th>
+ {{.lastCommit}}
+ </th>
+ </tr>
+ </thead>
+ <tbody>{{range .Repositories}}
+ <tr>
+ <td>
+ <a href="{{.name}}">
+ {{.name}}
+ </a>
+ </td>
+ <td>
+ {{.description}}
+ </td>
+ <td>
+ <time datetime="{{.lastCommitDate}}">{{.lastCommitDate}}</time>
+ </td>
+ </tr>{{end}}
+ </tbody>
+ </table>
+ </main>
+ <footer>
+ <hr />
+ <p>
+ {{.generatedWith}} <a href="https://euandre.org/s/gistatic/">gistatic</a>
+ </p>
+ </footer>
+ </body>
+ </html>
+ `)))
+
+ branchesHTML = g.Must(template.New("").Parse(g.Heredoc(`
+ <!DOCTYPE html>
+ <html lang="{{.language}}">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <meta name="description" content="{{.metadata.description}}" />
+ <link rel="icon" type="image/svg+xml" href="../img/favicon.svg" />
+ <link rel="stylesheet" type="text/css" href="../style.css" />
+ <title>{{.branches}} ยท {{.metadata.name}}</title>
+ </head>
+ <body>
+ <header>
+ <div class="header-horizontal-grouping">
+ <a href="../">
+ <picture>
+ <source srcset="../img/logo/dark.svg" media="(prefers-color-scheme: dark)" />
+ <img src="../img/logo/light.svg" alt="{{.logoAlt}}" />
+ </picture>
+ </a>
+ <div class="header-description">
+ <h1>
+ <a href="./">
+ {{.metadata.name}}
+ </a>
+ </h1>
+ <h2>
+ {{.metadata.description}}
+ </h2>{{if .cloneURL}}
+ <code>
+ git clone {{.cloneURL}}/{{.metadata.name}}/
+ </code>{{end}}
+ </div>
+ </div>
+ <nav>
+ <ul>
+ <li class="selected-nav-item">
+ <a href="branches.html">
+ {{.branches}}
+ </a>
+ </li>
+ <li>
+ <a href="tags.html">
+ {{.tags}}
+ </a>
+ </li>
+ <li>
+ <a href="{{.log}}/">
+ {{.log}}
+ </a>
+ </li>
+ </ul>
+ </nav>
+ <hr />
+ </header>
+ <main>
+ <table>
+ <thead>
+ <tr>
+ <th>
+ {{.branch}}
+ </th>
+ <th>
+ {{.commitMessage}}
+ </th>
+ <th>
+ {{.author}}
+ </th>
+ <th>
+ {{.date}}
+ </th>
+ </tr>
+ </thead>
+ <tbody>{{range .data.branches}}
+ <tr>
+ <td>
+ <a href="{{$.log}}/{{.name}}.html">
+ {{.name}}
+ </a>
+ </td>
+ <td>
+ <a href="{{$.commit}}/{{.sha}}.html">
+ {{.messageSummary}}
+ </a>
+ </td>
+ <td>
+ {{.author}}
+ </td>
+ <td>
+ <time datetime="{{.date}}">{{.date}}</time>
+ </td>
+ </tr>{{end}}
+ </tbody>
+ </table>
+ </main>
+ <footer>
+ <hr />
+ <p>
+ {{.generatedWith}} <a href="https://euandre.org/s/gistatic/">gistatic</a>
+ </p>
+ </footer>
+ </body>
+ </html>
+ `)))
+
+ tagsHTML = g.Must(template.New("").Parse(g.Heredoc(`
+ <!DOCTYPE html>
+ <html lang="{{.language}}">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <meta name="description" content="{{.pageDescription}}" />
+ <link rel="icon" type="image/svg+xml" href="img/favicon.svg" />
+ <link rel="stylesheet" type="text/css" href="style.css" />
+ <title>{{.title}}</title>
+ </head>
+ <body>
+ <header>
+ <div class="header-horizontal-grouping">
+ <picture>
+ <source srcset="img/logo/dark.svg" media="(prefers-color-scheme: dark)" />
+ <img src="img/logo/light.svg" alt="{{.logoAlt}}" />
+ </picture>
+ <h1 class="header-description">
+ {{.title}}
+ </h1>
+ </div>
+ <hr />
+ </header>
+ <main>
+ <table>
+ <thead>
+ <tr>
+ <th>
+ {{.name}}
+ </th>
+ <th>
+ {{.description}}
+ </th>
+ <th>
+ {{.lastCommit}}
+ </th>
+ </tr>
+ </thead>
+ <tbody>{{range .Repositories}}
+ <tr>
+ <td>
+ <a href="{{.name}}">
+ {{.name}}
+ </a>
+ </td>
+ <td>
+ {{.description}}
+ </td>
+ <td>
+ <time datetime="{{.lastCommitDate}}">{{.lastCommitDate}}</time>
+ </td>
+ </tr>{{end}}
+ </tbody>
+ </table>
+ </main>
+ <footer>
+ <hr />
+ <p>
+ {{.generatedWith}} <a href="https://euandre.org/s/gistatic/">gistatic</a>
+ </p>
+ </footer>
+ </body>
+ </html>
+ `)))
+
+ logHTML = g.Must(template.New("").Parse(g.Heredoc(`
+ <!DOCTYPE html>
+ <html lang="{{.language}}">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <meta name="description" content="{{.pageDescription}}" />
+ <link rel="icon" type="image/svg+xml" href="img/favicon.svg" />
+ <link rel="stylesheet" type="text/css" href="style.css" />
+ <title>{{.title}}</title>
+ </head>
+ <body>
+ <header>
+ <div class="header-horizontal-grouping">
+ <picture>
+ <source srcset="img/logo/dark.svg" media="(prefers-color-scheme: dark)" />
+ <img src="img/logo/light.svg" alt="{{.logoAlt}}" />
+ </picture>
+ <h1 class="header-description">
+ {{.title}}
+ </h1>
+ </div>
+ <hr />
+ </header>
+ <main>
+ <table>
+ <thead>
+ <tr>
+ <th>
+ {{.name}}
+ </th>
+ <th>
+ {{.description}}
+ </th>
+ <th>
+ {{.lastCommit}}
+ </th>
+ </tr>
+ </thead>
+ <tbody>{{range .Repositories}}
+ <tr>
+ <td>
+ <a href="{{.name}}">
+ {{.name}}
+ </a>
+ </td>
+ <td>
+ {{.description}}
+ </td>
+ <td>
+ <time datetime="{{.lastCommitDate}}">{{.lastCommitDate}}</time>
+ </td>
+ </tr>{{end}}
+ </tbody>
+ </table>
+ </main>
+ <footer>
+ <hr />
+ <p>
+ {{.generatedWith}} <a href="https://euandre.org/s/gistatic/">gistatic</a>
+ </p>
+ </footer>
+ </body>
+ </html>
+ `)))
+
+ commitHTML = g.Must(template.New("").Parse(g.Heredoc(`
+ <!DOCTYPE html>
+ <html lang="{{.language}}">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <meta name="description" content="{{.pageDescription}}" />
+ <link rel="icon" type="image/svg+xml" href="img/favicon.svg" />
+ <link rel="stylesheet" type="text/css" href="style.css" />
+ <title>{{.title}}</title>
+ </head>
+ <body>
+ <header>
+ <div class="header-horizontal-grouping">
+ <picture>
+ <source srcset="img/logo/dark.svg" media="(prefers-color-scheme: dark)" />
+ <img src="img/logo/light.svg" alt="{{.logoAlt}}" />
+ </picture>
+ <h1 class="header-description">
+ {{.title}}
+ </h1>
+ </div>
+ <hr />
+ </header>
+ <main>
+ <table>
+ <thead>
+ <tr>
+ <th>
+ {{.name}}
+ </th>
+ <th>
+ {{.description}}
+ </th>
+ <th>
+ {{.lastCommit}}
+ </th>
+ </tr>
+ </thead>
+ <tbody>{{range .Repositories}}
+ <tr>
+ <td>
+ <a href="{{.name}}">
+ {{.name}}
+ </a>
+ </td>
+ <td>
+ {{.description}}
+ </td>
+ <td>
+ <time datetime="{{.lastCommitDate}}">{{.lastCommitDate}}</time>
+ </td>
+ </tr>{{end}}
+ </tbody>
+ </table>
+ </main>
+ <footer>
+ <hr />
+ <p>
+ {{.generatedWith}} <a href="https://euandre.org/s/gistatic/">gistatic</a>
+ </p>
+ </footer>
+ </body>
+ </html>
+ `)))
+
+ sourceHTML = g.Must(template.New("").Parse(g.Heredoc(`
+ <!DOCTYPE html>
+ <html lang="{{.language}}">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <meta name="description" content="{{.pageDescription}}" />
+ <link rel="icon" type="image/svg+xml" href="img/favicon.svg" />
+ <link rel="stylesheet" type="text/css" href="style.css" />
+ <title>{{.title}}</title>
+ </head>
+ <body>
+ <header>
+ <div class="header-horizontal-grouping">
+ <picture>
+ <source srcset="img/logo/dark.svg" media="(prefers-color-scheme: dark)" />
+ <img src="img/logo/light.svg" alt="{{.logoAlt}}" />
+ </picture>
+ <h1 class="header-description">
+ {{.title}}
+ </h1>
+ </div>
+ <hr />
+ </header>
+ <main>
+ <table>
+ <thead>
+ <tr>
+ <th>
+ {{.name}}
+ </th>
+ <th>
+ {{.description}}
+ </th>
+ <th>
+ {{.lastCommit}}
+ </th>
+ </tr>
+ </thead>
+ <tbody>{{range .repositories}}
+ <tr>
+ <td>
+ <a href="{{.name}}">
+ {{.name}}
+ </a>
+ </td>
+ <td>
+ {{.description}}
+ </td>
+ <td>
+ <time datetime="{{.lastCommitDate}}">{{.lastCommitDate}}</time>
+ </td>
+ </tr>{{end}}
+ </tbody>
+ </table>
+ </main>
+ <footer>
+ <hr />
+ <p>
+ {{.generatedWith}} <a href="https://euandre.org/s/gistatic/">gistatic</a>
+ </p>
+ </footer>
+ </body>
+ </html>
+ `)))
+
+)
+
+
+
+type repositoryTemplateT struct{
+ template *template.Template
+ path string
+ keys map[string]interface{}
+}
+
type argsT struct{
- outputPath string
- cloneUrl string
- allArgs []string
+ 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",
+ "Usage: %s [-o DIRECTORY] [-u CLONE_URL] REPOSITORY...\n",
argv0,
)
}
@@ -32,12 +1062,13 @@ func getopt(allArgs []string, w io.Writer) (argsT, int) {
fs := flag.NewFlagSet("", flag.ContinueOnError)
fs.Usage = func() {}
fs.SetOutput(w)
- outputPath := fs.String(
+
+ outdir := fs.String(
"o",
- g.Must(os.Getwd()),
- "The directory where to write the generated files",
+ g.Must(fn_wd()),
+ "Where to store the generated files",
)
- cloneUrl := fs.String(
+ cloneURL := fs.String(
"u",
"",
"The prefix of the online cloning addresss",
@@ -48,27 +1079,20 @@ func getopt(allArgs []string, w io.Writer) (argsT, int) {
}
subArgs := fs.Args()
- if len(subArgs) == 0 {
- fmt.Fprintf(w, "Missing DIRECTORY.\n")
- usage(argv0, w)
- return argsT{}, 2
- }
return argsT{
- outputPath: *outputPath,
- cloneUrl: *cloneUrl,
- allArgs: allArgs,
+ allArgs: allArgs,
+ subArgs: subArgs,
+ cloneURL: *cloneURL,
+ outdir: *outdir,
}, 0
}
-func run(args argsT, stdin io.Reader, stdout io.Writer, stderr io.Writer) int {
- return 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))