shroomgit

generate static pages of git repos
git clone https://git.davidvoz.net/shroomgit.git
index
logs
tree

commit 39ab9877330c2743e9fe8f6d206836a2143bcb35
Author: David Voznyarskiy <davidv@no-reply@disroot.org>
Date:   Fri Apr 3 22:00:48 2026 -0700

    lot of refactoring due to a strange conflict I made
    
    Signed-off-by: David Voznyarskiy <davidv@no-reply@disroot.org>

diff --git a/Makefile b/Makefile
index f81cb3c..923eae4 100644
--- a/Makefile
+++ b/Makefile
@@ -4,7 +4,7 @@ all: shindex shgit shindex: @mkdir -p bin
- go build -o bin/shindex shindex/shindex.go
+ go build -o bin/shindex ./shindex
shgit: @mkdir -p bin diff --git a/config/config.go b/config/config.go deleted file mode 100644 index e054c37..0000000
--- a/config/config.go
+++ /dev/null
@@ -1,41 +0,0 @@
-// Package config
-package config
-
-type Config struct {
- Title string
- Favicon string
- Logo string
- LogoHref string
- LogoWidth int
- LogoHeight int
- Desc string
- URL string
-}
-
-// Shindex config
-var Shindex = Config{
- Title: "Repositories",
- Logo: "logo.png",
- LogoHref: "https://davidvoz.net",
- LogoWidth: 88,
- LogoHeight: 36,
- Desc: "this is a static index of my repos, for more visit <a href='https://git.disroot.org/davidv'>here</a>",
-}
-
-// Shgit config
-var Shgit = Config{
- Favicon: "logo.png",
- Logo: "logo.png",
- LogoHref: "..",
- LogoWidth: 40,
- LogoHeight: 40,
- URL: "https://git.davidvoz.net/",
-}
-
-var Style string = `
- .codeline {
- text-align: right;
- vertical-align: top;
- user-select: none;
- }
-`
diff --git a/shgit/config.go b/shgit/config.go new file mode 100644 index 0000000..15d10f5
--- /dev/null
+++ b/shgit/config.go
@@ -0,0 +1,29 @@
+package main
+
+type config struct {
+ favicon string
+ logo string
+ logoHref string
+ logoWidth int
+ logoHeight int
+ url string
+}
+
+var shgit = config{
+ favicon: "logo.png",
+ logo: "logo.png",
+ logoHref: "..",
+ logoWidth: 40,
+ logoHeight: 40,
+ url: "https://git.davidvoz.net/",
+}
+
+var styles = map[string]string{
+ "codeline": `
+ .codeline {
+ text-align: right;
+ vertical-align: top;
+ user-select: none;
+ }
+ `,
+}
diff --git a/shgit/logs.go b/shgit/logs.go new file mode 100644 index 0000000..53607d0
--- /dev/null
+++ b/shgit/logs.go
@@ -0,0 +1,209 @@
+package main
+
+import (
+ "fmt"
+ "html"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "strings"
+)
+
+// overall awful
+func logTest(repo string, path string) {
+ topPart := genTopPart(repo, 1)
+ logsDir := filepath.Join(path, "logs")
+
+ err := os.MkdirAll(logsDir, 0o755)
+ if err != nil {
+ panic(err)
+ }
+
+ cmd, err := exec.Command("git", "-C", repo, "rev-list", "--reverse", "--date-order", "HEAD").Output()
+ if err != nil {
+ panic(err)
+ }
+
+ hashes := strings.Split(strings.TrimSpace(string(cmd)), "\n")
+
+ // TODO fix formatting
+ // TODO add coloring and links
+ for _, hash := range hashes {
+ cmd, err := exec.Command(
+ "git",
+ "-C",
+ repo,
+ "show",
+ hash,
+ ).Output()
+ if err != nil {
+ panic(err)
+ }
+
+ lines := strings.Split(string(cmd), "\n")
+ var firstLine string
+ var hash string
+ if lines[0] != "" {
+ firstLine = lines[0]
+ } else {
+ continue
+ }
+
+ fields := strings.Fields(firstLine)
+ hash = fields[1]
+
+ outputFile := filepath.Join(logsDir, fmt.Sprintf("%s.html", hash))
+ file, _ := os.Create(outputFile)
+ defer file.Close()
+
+ file.WriteString(topPart)
+
+ commitPageWriter(file, lines)
+ }
+}
+
+func commitPageWriter(file *os.File, lines []string) {
+ file.WriteString("<pre style=\"padding:6px;border-radius:4px;font-family:monospace;\">\n")
+
+ for _, line := range lines {
+ escaped := html.EscapeString(line)
+ if strings.HasPrefix(line, "+") {
+ file.WriteString("<div style=\"color:ForestGreen\">")
+ file.WriteString(escaped)
+ file.WriteString("</div>")
+ } else if strings.HasPrefix(line, "-") {
+ file.WriteString("<div style=\"color:#e54e50\">")
+ file.WriteString(escaped)
+ file.WriteString("</div>")
+
+ } else {
+ file.WriteString(escaped)
+ file.WriteString("\n")
+ }
+ }
+
+ file.WriteString("</pre>\n")
+}
+
+func logPage(topPart string, repo string, path string) {
+ outputFile := filepath.Join(path, "log.html")
+ file, err := os.Create(outputFile)
+ if err != nil {
+ panic(err)
+ }
+ defer file.Close()
+
+ cmd, err := exec.Command(
+ "git",
+ "-C",
+ repo,
+ "rev-list",
+ "--count",
+ "HEAD",
+ ).Output()
+ if err != nil {
+ return
+ }
+
+ numofCommitsStr := strings.TrimSpace(string(cmd))
+ numofCommits, _ := strconv.Atoi(numofCommitsStr)
+
+ file.WriteString(topPart)
+ file.WriteString("<table>")
+
+ for i := range numofCommits {
+ file.WriteString("<tr>")
+
+ cmd, _ := exec.Command(
+ "git",
+ "-C", repo,
+ "log", "-1",
+ fmt.Sprintf("--skip=%d", i),
+ "--pretty=format:%cd",
+ "--date=format:%Y-%m-%d",
+ ).Output()
+ date := strings.TrimSpace(string(cmd))
+ file.WriteString("<td valign=\"top\">")
+ file.WriteString(date)
+
+ file.WriteString("</td>\n")
+
+ cmd, _ = exec.Command(
+ "git",
+ "-C", repo,
+ "show",
+ fmt.Sprintf("HEAD~%d", i),
+ ).Output()
+
+ // commit := strings.TrimSpace(string(cmd))
+
+ lines := strings.Split(string(cmd), "\n")
+ var firstLine string
+ var hash string
+ if lines[0] != "" {
+ firstLine = lines[0]
+ } else {
+ return
+ }
+
+ fields := strings.Fields(firstLine)
+ hash = fields[1]
+
+ cmd, _ = exec.Command(
+ "git",
+ "-C", repo,
+ "log", "-1",
+ fmt.Sprintf("--skip=%d", i),
+ "--pretty=format:%s",
+ ).Output()
+ commit := strings.TrimSpace(string(cmd))
+ file.WriteString("<td>")
+ file.WriteString("\n<a href=\"logs/")
+ file.WriteString(hash)
+ file.WriteString(".html\">" + commit + "</a>\n")
+ file.WriteString("</td>\n")
+
+ cmd, _ = exec.Command(
+ "git",
+ "-C", repo,
+ "log", "-1",
+ fmt.Sprintf("--skip=%d", i),
+ "--pretty=format:%an",
+ ).Output()
+ author := strings.TrimSpace(string(cmd))
+ file.WriteString("<td style=\"white-space:nowrap\" valign=\"top\">")
+ file.WriteString(author)
+ file.WriteString("</td>\n")
+
+ cmd, _ = exec.Command(
+ "git",
+ "-C", repo,
+ "log", "-1",
+ fmt.Sprintf("--skip=%d", i),
+ "--shortstat",
+ "--pretty=format:",
+ ).Output()
+ cmdOutput := strings.TrimSpace(string(cmd))
+ arr := strings.Fields(cmdOutput)
+ if len(arr) == 0 {
+ continue
+ }
+ file.WriteString("<td style=\"padding-left: 1em\" valign=\"top\" align=\"right\">")
+ file.WriteString(arr[0])
+ file.WriteString("</td>\n")
+ file.WriteString("<td style=\"color:ForestGreen\" valign=\"top\" align=\"right\">+")
+ file.WriteString(arr[3])
+ file.WriteString("</td>\n")
+ if len(arr) > 5 {
+ file.WriteString("<td style=\"color:Crimson\" valign=\"top\" align=\"right\">-")
+ file.WriteString(arr[5])
+ file.WriteString("</td>\n")
+ }
+
+ file.WriteString("\n</tr>")
+ }
+
+ file.WriteString("\n</table>")
+ file.WriteString("\n</body>")
+}
diff --git a/shgit/shared.go b/shgit/shared.go index 0f112c6..00f78fd 100644
--- a/shgit/shared.go
+++ b/shgit/shared.go
@@ -7,12 +7,10 @@ import ( "path/filepath" "strconv" "strings"
-
- "shroomgit/config"
) // prints an html section for each file inputted
-// TODO class option for lines instead of the clunky style elements
+// TODO maybe see if you can add pandoc program coloring theme to it
func writePerFile(file *os.File, path string) { notFile, _ := os.Open(path) defer notFile.Close() @@ -21,6 +19,9 @@ func writePerFile(file *os.File, path string) { numLine := 1 var strNumLine string
+ fileName := filepath.Base(path)
+ file.WriteString("<b>" + fileName + "</b>")
+
file.WriteString("\n<table style=\"white-space:pre\">") for scanner.Scan() { line := scanner.Text() @@ -46,22 +47,28 @@ func writePerFile(file *os.File, path string) { } // the common HTML lines that all files will share
-var genTopPart = func(repo string) string {
+var genTopPart = func(repo string, depth int) string {
var b strings.Builder
- shgitStyle := config.Style
- config := config.Shgit
+ config := shgit
repoName, desc := getRepoNameAndDesc(repo) b.WriteString("<!DOCTYPE html>\n") b.WriteString("<html>\n") b.WriteString("<head>\n")
- b.WriteString("<link rel=\"stylesheet\" href=\"style.css\" \\>\n")
+ b.WriteString("<link rel=\"stylesheet\" href=\"")
+ for range depth {
+ b.WriteString("../")
+ }
+ b.WriteString("style.css\" \\>\n")
b.WriteString("<link rel=\"icon\" type=\"image/png\" href=\"")
- b.WriteString(config.Favicon)
+ for range depth {
+ b.WriteString("../")
+ }
+ b.WriteString(config.favicon)
b.WriteString("\" \\>\n") b.WriteString("<style>")
- b.WriteString(shgitStyle)
+ b.WriteString(styles["codeline"])
b.WriteString("</style>\n") b.WriteString("</head>\n") @@ -71,19 +78,27 @@ var genTopPart = func(repo string) string { b.WriteString("<table>") b.WriteString("<tr>\n")
- if config.Logo != "" {
+ if config.logo != "" {
b.WriteString("<td>") b.WriteString("<a href=\"")
- b.WriteString(config.LogoHref)
+ for range depth {
+ b.WriteString("../")
+ }
+
+ b.WriteString(config.logoHref)
b.WriteString("\">") b.WriteString("<img src=\"")
- b.WriteString(config.Logo)
+ for range depth {
+ b.WriteString("../")
+ }
+
+ b.WriteString(config.logo)
b.WriteString("\" width=")
- b.WriteString(strconv.Itoa(config.LogoWidth))
+ b.WriteString(strconv.Itoa(config.logoWidth))
b.WriteString(" height=")
- b.WriteString(strconv.Itoa(config.LogoHeight))
+ b.WriteString(strconv.Itoa(config.logoHeight))
b.WriteString("/>") b.WriteString("</a>") b.WriteString("</td>") @@ -103,7 +118,7 @@ var genTopPart = func(repo string) string { b.WriteString("<table>\n") b.WriteString("<tr>\n<td>\n<code style=\"background-color: #222; color: white; padding: 4px; user-select: all; border: none;\">")
- b.WriteString("git clone " + config.URL + repoName + ".git")
+ b.WriteString("git clone " + config.url + repoName + ".git")
b.WriteString("\n</code></td>") b.WriteString("</tr>\n") @@ -112,31 +127,53 @@ var genTopPart = func(repo string) string { b.WriteString("\n<div style=\"display:flex; flex-wrap:wrap; gap:30px;margin-top:3px;\">\n") b.WriteString("\n<div style=\"display:flex;padding-left:20px;\">\n")
- b.WriteString("<a href=\"index.html\">")
+ b.WriteString("<a href=\"")
+ for range depth {
+ b.WriteString("../")
+ }
+ b.WriteString("index.html\">")
b.WriteString("index\n") b.WriteString("</a>") b.WriteString("</div>\n") b.WriteString("\n<div style=\"display:flex;\">\n")
- b.WriteString("<a href=\"log.html\">")
+ b.WriteString("<a href=\"")
+ for range depth {
+ b.WriteString("../")
+ }
+ b.WriteString("log.html\">")
b.WriteString("logs\n") b.WriteString("</a>") b.WriteString("</div>\n") b.WriteString("\n<div style=\"display:flex;\">\n")
- b.WriteString("<a>")
+ b.WriteString("<a href=\"")
+ for range depth {
+ b.WriteString("../")
+ }
+ b.WriteString("tree.html\">")
b.WriteString("tree\n") b.WriteString("</a>") b.WriteString("</div>\n")
- // TODO if license exists {
- if 1+1 == 2 {
- b.WriteString("\n<div style=\"display:flex;\">\n")
- b.WriteString("<a href=\"license.html\">")
- b.WriteString("license\n")
- b.WriteString("</a>")
- b.WriteString("</div>\n")
-
+ files := []string{"LICENSE", "LICENSE.txt", "LICENSE.md", "license", "COPYING"}
+
+ for _, f := range files {
+ possibleL := filepath.Join(repo, f)
+ if _, err := os.ReadFile(possibleL); err == nil {
+ b.WriteString("\n<div style=\"display:flex;\">\n")
+ b.WriteString("<a href=\"")
+ for range depth {
+ b.WriteString("../")
+ }
+ b.WriteString("files/")
+ b.WriteString(filepath.Base(possibleL))
+ b.WriteString(".html\">")
+ b.WriteString("license\n")
+ b.WriteString("</a>")
+ b.WriteString("</div>\n")
+ break
+ }
} b.WriteString("\n</div>\n") @@ -165,6 +202,9 @@ func getRepoNameAndDesc(repo string) (string, string) { line = strings.TrimSpace(line) if strings.HasPrefix(line, "url =") { parts := strings.Fields(line)
+ if len(parts) < 3 {
+ continue
+ }
url := parts[2] url = strings.TrimSuffix(url, ".git") segments := strings.Split(url, "/") @@ -174,3 +214,4 @@ func getRepoNameAndDesc(repo string) (string, string) { return filepath.Base(repo), desc }
+
diff --git a/shgit/shgit.go b/shgit/shgit.go index 881c0b3..e2bd1aa 100644
--- a/shgit/shgit.go
+++ b/shgit/shgit.go
@@ -193,3 +193,4 @@ func indexPageCommitTable(numofCommits int, file *os.File, repo string, path str file.WriteString("</table>") file.WriteString("</body>") }
+
diff --git a/shgit/tree.go b/shgit/tree.go new file mode 100644 index 0000000..8eace16
--- /dev/null
+++ b/shgit/tree.go
@@ -0,0 +1,90 @@
+package main
+
+import (
+ "fmt"
+ "io/fs"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+// TODO make it more tree like
+func treeRepo(topPart string, repo string, output string) {
+ logsDir := filepath.Join(output, "files")
+
+ err := os.MkdirAll(logsDir, 0o755)
+ if err != nil {
+ panic(err)
+ }
+
+ outputFile := filepath.Join(output, "tree.html")
+ file, err := os.Create(outputFile)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer file.Close()
+
+ file.WriteString(topPart)
+
+ file.WriteString("<table>\n")
+
+ file.WriteString("\n<tr>")
+ file.WriteString("<td>Mode</td>")
+ file.WriteString("<td>Bytes</td>")
+ file.WriteString("</tr>")
+
+ err = filepath.WalkDir(repo, func(path string, d fs.DirEntry, err error) error {
+ relPath, _ := filepath.Rel(repo, path)
+
+ if d.Name() == ".git" {
+ return filepath.SkipDir
+ } else if relPath == "." {
+ return nil
+ } else if d.IsDir() {
+ return nil
+ }
+ info, _ := d.Info()
+
+ file.WriteString("<tr>")
+ file.WriteString("<td>")
+ file.WriteString(info.Mode().String())
+ file.WriteString("&nbsp;</td>")
+ file.WriteString("<td valign=\"top\" align=\"right\">")
+ fmt.Fprintf(file, " %d", info.Size())
+ file.WriteString("</td>\n")
+ file.WriteString("<td>")
+ file.WriteString("<a href=\"files/")
+ file.WriteString(relPath)
+ file.WriteString(".html\">")
+ file.WriteString(relPath)
+ treeFiles(path, output, relPath, repo)
+ file.WriteString("</a>")
+ file.WriteString("</td>")
+
+ file.WriteString("</tr>")
+
+ return nil
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ file.WriteString("</table>\n")
+
+ file.WriteString("</body>\n")
+ file.WriteString("</html>")
+}
+
+func treeFiles(path string, output string, relPath string, repo string) {
+ outputFile := filepath.Join(output, "files", fmt.Sprintf("%s.html", relPath))
+ os.MkdirAll(filepath.Dir(outputFile), 0o755)
+ file, _ := os.Create(outputFile)
+ defer file.Close()
+
+ depth := strings.Count(relPath, "/")
+ topPart := genTopPart(repo, depth+1)
+
+ file.WriteString(topPart)
+ writePerFile(file, path)
+}
diff --git a/shindex/config.go b/shindex/config.go new file mode 100644 index 0000000..676cf3a
--- /dev/null
+++ b/shindex/config.go
@@ -0,0 +1,20 @@
+package main
+
+type config struct {
+ title string
+ logo string
+ logoHref string
+ logoWidth int
+ logoHeight int
+ desc string
+}
+
+// Shindex config
+var shindex = config{
+ title: "Repositories",
+ logo: "logo.png",
+ logoHref: "https://davidvoz.net",
+ logoWidth: 88,
+ logoHeight: 36,
+ desc: "this is a static index of my repos, for more visit <a href='https://git.disroot.org/davidv'>here</a>",
+}
diff --git a/shindex/shindex.go b/shindex/shindex.go index 9640a8d..5c20994 100644
--- a/shindex/shindex.go
+++ b/shindex/shindex.go
@@ -6,8 +6,6 @@ import ( "os/exec" "path/filepath" "strings"
-
- "shroomgit/config"
) func main() { @@ -43,15 +41,14 @@ func gitTable(args []string) { fmt.Println("<div style=\"width:32px;height:32px;margin-right:8px;\">") // the line below assumes the directory path is the same as the repo's name, not arguments
- // change the 'repoName' to 'repo' if you want the latter option
- fmt.Printf("<a href=\"%s/log.html\" style=\"color:inherit\">", repoName)
+ // change the 'repoName' to 'repo' if you want the latter option for the line below
+ // other instances of such a case would have to be changed as well
+ fmt.Printf("<a href=\"%s/index.html\" style=\"color:inherit\">", repoName)
fmt.Println("<svg width=\"32\" height=\"32\">") fmt.Println("<rect x=\"6\" y=\"10\" width=\"20\" height=\"20\"") fmt.Println("fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" transform=\"rotate(45 20 20)\"/></svg></a></div><div>")
-
- // other instance of possible path argument issue
- fmt.Printf("<a href=\"%s/log.html\" style=\"line-height:1.5;\">", repoName)
+ fmt.Printf("<a href=\"%s/index.html\" style=\"line-height:1.5;\">", repoName)
fmt.Printf("<b>%s", repoName) fmt.Printf("</b></a><br>%s", desc) @@ -69,25 +66,25 @@ func gitTable(args []string) { } func basicPart() {
- config := config.Shindex
+ config := shindex
fmt.Println("<!DOCTYPE html>") fmt.Println("<html>") fmt.Println("<head>")
- fmt.Printf("<title>%s</title>\n", config.Title)
+ fmt.Printf("<title>%s</title>\n", config.title)
fmt.Println("<link rel=\"stylesheet\" href=\"style.css\" \\>") fmt.Println("<link rel=\"icon\" type=\"image/png\" href=\"favicon.png\" \\>") fmt.Println("</head>") fmt.Println("<body>") fmt.Println("<table>")
- fmt.Printf("<tr><td><a href=\"%s\">", config.LogoHref)
- fmt.Printf("<img src=\"%s\" ", config.Logo)
- fmt.Printf("alt=\"\" width=%d ", config.LogoWidth)
- fmt.Printf("height=%d ", config.LogoHeight)
+ fmt.Printf("<tr><td><a href=\"%s\">", config.logoHref)
+ fmt.Printf("<img src=\"%s\" ", config.logo)
+ fmt.Printf("alt=\"\" width=%d ", config.logoWidth)
+ fmt.Printf("height=%d ", config.logoHeight)
fmt.Print("/></a></td>\n")
- fmt.Printf("<td>%s", config.Title)
+ fmt.Printf("<td>%s", config.title)
fmt.Println("</td></tr>") fmt.Println("</table>")
- fmt.Println(config.Desc)
+ fmt.Println(config.desc)
fmt.Println("<hr>") } @@ -153,6 +150,7 @@ func getRepoNameAndDesc(repo string) (string, string) { return filepath.Base(repo), desc }
+// TODO add more errors messages
func printHelp(errorNum int) { switch errorNum { case 1: @@ -162,3 +160,4 @@ func printHelp(errorNum int) { fmt.Println("Usage: shindex [repo_path...]") fmt.Println("creates a static html page to display git repos") }
+