From 04c91c40d548bb70062fcadeeeea37fd4319e066 Mon Sep 17 00:00:00 2001 From: EuAndreh Date: Sun, 11 May 2025 07:58:03 -0300 Subject: Finish branches.html and setup i18n of manpages and HTML strings --- src/gistatic.go | 1064 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 1044 insertions(+), 20 deletions(-) (limited to 'src/gistatic.go') 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(` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `) + + 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}} + + +
+
+ + + {{.logoAlt}} + +

+ {{.title}} +

+
+
+
+
+ + + + + + + + + {{range .repositories}} + + + + + {{end}} + +
+ {{.name}} + + {{.description}} + + {{.lastCommit}} +
+ + {{.name}} + + + {{.description}} + + +
+
+ + + + `))) + + repoindexHTML = g.Must(template.New("").Parse(g.Heredoc(` + + + + + + + + + {{.title}} + + +
+
+ + + {{.logoAlt}} + +

+ {{.title}} +

+
+
+
+
+ + + + + + + + + {{range .Repositories}} + + + + + {{end}} + +
+ {{.name}} + + {{.description}} + + {{.lastCommit}} +
+ + {{.name}} + + + {{.description}} + + +
+
+ + + + `))) + + branchesHTML = g.Must(template.New("").Parse(g.Heredoc(` + + + + + + + + + {{.branches}} ยท {{.metadata.name}} + + +
+
+ + + + {{.logoAlt}} + + +
+

+ + {{.metadata.name}} + +

+

+ {{.metadata.description}} +

{{if .cloneURL}} + + git clone {{.cloneURL}}/{{.metadata.name}}/ + {{end}} +
+
+ +
+
+
+ + + + + + + + + + {{range .data.branches}} + + + + + + {{end}} + +
+ {{.branch}} + + {{.commitMessage}} + + {{.author}} + + {{.date}} +
+ + {{.name}} + + + + {{.messageSummary}} + + + {{.author}} + + +
+
+ + + + `))) + + tagsHTML = g.Must(template.New("").Parse(g.Heredoc(` + + + + + + + + + {{.title}} + + +
+
+ + + {{.logoAlt}} + +

+ {{.title}} +

+
+
+
+
+ + + + + + + + + {{range .Repositories}} + + + + + {{end}} + +
+ {{.name}} + + {{.description}} + + {{.lastCommit}} +
+ + {{.name}} + + + {{.description}} + + +
+
+ + + + `))) + + logHTML = g.Must(template.New("").Parse(g.Heredoc(` + + + + + + + + + {{.title}} + + +
+
+ + + {{.logoAlt}} + +

+ {{.title}} +

+
+
+
+
+ + + + + + + + + {{range .Repositories}} + + + + + {{end}} + +
+ {{.name}} + + {{.description}} + + {{.lastCommit}} +
+ + {{.name}} + + + {{.description}} + + +
+
+ + + + `))) + + commitHTML = g.Must(template.New("").Parse(g.Heredoc(` + + + + + + + + + {{.title}} + + +
+
+ + + {{.logoAlt}} + +

+ {{.title}} +

+
+
+
+
+ + + + + + + + + {{range .Repositories}} + + + + + {{end}} + +
+ {{.name}} + + {{.description}} + + {{.lastCommit}} +
+ + {{.name}} + + + {{.description}} + + +
+
+ + + + `))) + + sourceHTML = g.Must(template.New("").Parse(g.Heredoc(` + + + + + + + + + {{.title}} + + +
+
+ + + {{.logoAlt}} + +

+ {{.title}} +

+
+
+
+
+ + + + + + + + + {{range .repositories}} + + + + + {{end}} + +
+ {{.name}} + + {{.description}} + + {{.lastCommit}} +
+ + {{.name}} + + + {{.description}} + + +
+
+ + + + `))) + +) + + + +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)) -- cgit v1.2.3