diff options
Diffstat (limited to 'src/gistatic.go')
-rw-r--r-- | src/gistatic.go | 1064 |
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)) |