Introduction
Introduction Statistics Contact Development Disclaimer Help
Performance improvements and standards updates also.. - staticgit - A git stati…
Log
Files
Refs
README
---
commit 0552fd260809624faa17bd5b9884cd5509db2062
parent b173c1cd71dbdf31b74ece626c89911d8fb566bc
Author: Jay Scott <[email protected]>
Date: Thu, 18 Jul 2024 23:46:56 +0100
Performance improvements and standards updates also..
- Refactoring the file structure to align to GO standards
- Adding cobra to handle flag parsing instead of flag.
- Broke out the HTML templates into files and using embed.
- Adding README
There is still code duplication in regards to git repo functions,
however, when I tried to optimize this by creating functions for git
open, head, commit and hash. I found that performance decreased by a
factor of two, so I have kept in the way I have it for now until I find
out why.
Diffstat:
M Makefile | 4 ++--
A README | 22 ++++++++++++++++++++++
A cmd/staticgit/staticgit.go | 39 +++++++++++++++++++++++++++++…
M go.mod | 5 ++++-
M go.sum | 8 ++++++++
A internal/config/config.go | 19 +++++++++++++++++++
A internal/repo/repo.go | 162 ++++++++++++++++++++++++++++++
A internal/site/build.go | 96 +++++++++++++++++++++++++++++…
A internal/template/template.go | 94 +++++++++++++++++++++++++++++…
A internal/template/templates/base.h… | 37 +++++++++++++++++++++++++++…
A internal/template/templates/detail… | 36 +++++++++++++++++++++++++++…
A internal/template/templates/index.… | 20 ++++++++++++++++++++
D main.go | 452 -----------------------------…
13 files changed, 539 insertions(+), 455 deletions(-)
---
diff --git a/Makefile b/Makefile
@@ -8,11 +8,11 @@ all: run
run:
@echo "Running $(APP_NAME)..."
- @go run . -p ./git -o tmp -i .ssh,jay.scot,internal-docs
+ @go run ./cmd/staticgit -p ./git -o tmp -i .ssh,jay.scot,internal-docs
build:
@echo "Building $(APP_NAME) for local architecture..."
- @go build $(GO_FLAGS) -o $(BUILD_DIR)/$(APP_NAME) .
+ @go build $(GO_FLAGS) -o $(BUILD_DIR)/$(APP_NAME) ./cmd/staticgit
@echo "Binary created at $(BUILD_DIR)/$(APP_NAME)"
fmt:
diff --git a/README b/README
@@ -0,0 +1,22 @@
+ __ ___ ___ __ __ ___
+/__` | /\ | | / ` / _` | |
+.__/ | /~~\ | | \__, \__> | |
+
+
+---
+
+StaticGit is a tool that generates static HTML pages from Git repositories. It
+can process multiple repositories and create an index page along with detailed
+pages for each repository. The site you are currently reading this on is
+created via StaticGit.
+
+
+Usage:
+ staticgit [flags]
+
+Flags:
+ -c, --commits int Max commits to display (default 100)
+ -h, --help help for staticgit
+ -i, --ignore strings Dirs to ignore (comma-separated)
+ -o, --out string Root path for output (default ".")
+ -p, --path string Path to git repos (required)
diff --git a/cmd/staticgit/staticgit.go b/cmd/staticgit/staticgit.go
@@ -0,0 +1,39 @@
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/spf13/cobra"
+ "staticgit/internal/config"
+ "staticgit/internal/site"
+)
+
+var (
+ cfg = &config.Config{}
+ rootCmd = &cobra.Command{
+ Use: "staticgit",
+ Short: "StaticGit generates static HTML pages from Git reposit…
+ Long: `StaticGit is a tool that generates static HTML pages fr…
+It can process multiple repositories and create an index page along with detai…
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return site.Build(cfg)
+ },
+ }
+)
+
+func init() {
+ rootCmd.Flags().StringVarP(&cfg.RepoDir, "path", "p", "", "Path to git…
+ rootCmd.Flags().StringVarP(&cfg.OutDir, "out", "o", ".", "Root path fo…
+ rootCmd.Flags().IntVarP(&cfg.MaxCommits, "commits", "c", 100, "Max com…
+ rootCmd.Flags().StringSliceVarP(&cfg.IgnoreDirs, "ignore", "i", []stri…
+
+ rootCmd.MarkFlagRequired("path")
+}
+
+func main() {
+ if err := rootCmd.Execute(); err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+}
diff --git a/go.mod b/go.mod
@@ -1,4 +1,4 @@
-module main
+module staticgit
go 1.22.5
@@ -14,11 +14,14 @@ require (
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // ind…
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // ind…
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indir…
github.com/skeema/knownhosts v1.2.2 // indirect
+ github.com/spf13/cobra v1.8.1 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/mod v0.12.0 // indirect
diff --git a/go.sum b/go.sum
@@ -13,6 +13,7 @@ github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73p…
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nn…
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5…
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0k…
+github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa4…
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH5…
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aR…
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHj…
@@ -36,6 +37,8 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9…
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4…
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elm…
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13…
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2Fq…
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJ…
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo…
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaARe…
@@ -57,11 +60,16 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYP…
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7g…
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISo…
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvd…
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiy…
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPL…
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQc…
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONW…
github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj4…
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nT…
+github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
+github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN…
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3…
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GF…
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7p…
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+8…
diff --git a/internal/config/config.go b/internal/config/config.go
@@ -0,0 +1,19 @@
+package config
+
+import (
+ "strings"
+)
+
+type Config struct {
+ RepoDir string
+ IgnoreDirs []string
+ OutDir string
+ MaxCommits int
+}
+
+func parseIgnoreList(dirs string) []string {
+ if dirs == "" {
+ return nil
+ }
+ return strings.Split(dirs, ",")
+}
diff --git a/internal/repo/repo.go b/internal/repo/repo.go
@@ -0,0 +1,162 @@
+package repo
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/plumbing/object"
+)
+
+type Repo struct {
+ Name string
+ Description string
+ LastMod time.Time
+ gitRepo *git.Repository
+}
+
+type Commit struct {
+ Hash string
+ Author string
+ Date string
+ Msg string
+ Added int
+ Removed int
+}
+
+func OpenRepo(path string) (*Repo, error) {
+ gitRepo, err := git.PlainOpen(path)
+ if err != nil {
+ return nil, fmt.Errorf("open repo: %w", err)
+ }
+
+ name := filepath.Base(path)
+ desc, _ := os.ReadFile(filepath.Join(path, "description"))
+
+ head, err := gitRepo.Head()
+ if err != nil {
+ return nil, fmt.Errorf("get HEAD: %w", err)
+ }
+
+ commit, err := gitRepo.CommitObject(head.Hash())
+ if err != nil {
+ return nil, fmt.Errorf("get commit object: %w", err)
+ }
+
+ return &Repo{
+ Name: name,
+ Description: strings.TrimSpace(string(desc)),
+ LastMod: commit.Committer.When,
+ gitRepo: gitRepo,
+ }, nil
+}
+
+func (r *Repo) GetCommits(maxCommits int) ([]Commit, error) {
+ iter, err := r.gitRepo.Log(&git.LogOptions{})
+ if err != nil {
+ return nil, fmt.Errorf("get commit log: %w", err)
+ }
+
+ var cs []Commit
+ err = iter.ForEach(func(c *object.Commit) error {
+ if len(cs) >= maxCommits {
+ return nil
+ }
+
+ stats, err := c.Stats()
+ if err != nil {
+ return fmt.Errorf("get commit stats: %w", err)
+ }
+
+ add, del := 0, 0
+ for _, stat := range stats {
+ add += stat.Addition
+ del += stat.Deletion
+ }
+
+ cs = append(cs, Commit{
+ Hash: c.Hash.String()[:7],
+ Author: c.Author.Name,
+ Date: c.Author.When.Format("02 Jan 2006 15:04:05"),
+ Msg: strings.Split(c.Message, "\n")[0],
+ Added: add,
+ Removed: del,
+ })
+
+ return nil
+ })
+
+ if err != nil {
+ return nil, fmt.Errorf("iterate commits: %w", err)
+ }
+
+ return cs, nil
+}
+
+func (r *Repo) GetFiles() ([]string, error) {
+ head, err := r.gitRepo.Head()
+ if err != nil {
+ return nil, fmt.Errorf("get HEAD: %w", err)
+ }
+
+ commit, err := r.gitRepo.CommitObject(head.Hash())
+ if err != nil {
+ return nil, fmt.Errorf("get commit object: %w", err)
+ }
+
+ tree, err := commit.Tree()
+ if err != nil {
+ return nil, fmt.Errorf("get tree: %w", err)
+ }
+
+ var fs []string
+ err = tree.Files().ForEach(func(f *object.File) error {
+ fs = append(fs, f.Name)
+ return nil
+ })
+ if err != nil {
+ return nil, fmt.Errorf("iterate files: %w", err)
+ }
+
+ sort.Strings(fs)
+ return fs, nil
+}
+
+func (r *Repo) GetReadme() (string, error) {
+ names := []string{"README.md", "README.txt", "README"}
+
+ head, err := r.gitRepo.Head()
+ if err != nil {
+ return "", fmt.Errorf("get HEAD: %w", err)
+ }
+
+ commit, err := r.gitRepo.CommitObject(head.Hash())
+ if err != nil {
+ return "", fmt.Errorf("get commit object: %w", err)
+ }
+
+ tree, err := commit.Tree()
+ if err != nil {
+ return "", fmt.Errorf("get tree: %w", err)
+ }
+
+ for _, name := range names {
+ file, err := tree.File(name)
+ if err != nil {
+ continue
+ }
+
+ content, err := file.Contents()
+ if err != nil {
+ return "", fmt.Errorf("read file contents: %w", err)
+ }
+
+ return content, nil
+ }
+
+ return "No README found!", nil
+}
diff --git a/internal/site/build.go b/internal/site/build.go
@@ -0,0 +1,96 @@
+package site
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "sync"
+
+ "staticgit/internal/config"
+ "staticgit/internal/repo"
+ "staticgit/internal/template"
+)
+
+func Build(cfg *config.Config) error {
+ dirs, err := os.ReadDir(cfg.RepoDir)
+ if err != nil {
+ return fmt.Errorf("read repos dir: %w", err)
+ }
+
+ var wg sync.WaitGroup
+ repoChan := make(chan *repo.Repo, len(dirs))
+ errChan := make(chan error, len(dirs))
+
+ for _, d := range dirs {
+ if d.IsDir() && !contains(cfg.IgnoreDirs, d.Name()) {
+ wg.Add(1)
+ go func(d os.DirEntry) {
+ defer wg.Done()
+ path := filepath.Join(cfg.RepoDir, d.Name())
+ r, err := repo.OpenRepo(path)
+ if err != nil {
+ errChan <- fmt.Errorf("open repo %s: %…
+ return
+ }
+ repoChan <- r
+
+ out := filepath.Join(cfg.OutDir, d.Name())
+ if err := os.MkdirAll(out, 0755); err != nil {
+ errChan <- fmt.Errorf("create dir for …
+ return
+ }
+
+ if err := generateRepoPage(r, out, cfg.MaxComm…
+ errChan <- fmt.Errorf("process %s: %w"…
+ }
+ }(d)
+ }
+ }
+
+ go func() {
+ wg.Wait()
+ close(repoChan)
+ close(errChan)
+ }()
+
+ var repos []*repo.Repo
+ for r := range repoChan {
+ repos = append(repos, r)
+ }
+
+ for err := range errChan {
+ fmt.Println(err)
+ }
+
+ return template.GenerateIndex(cfg.OutDir, repos)
+}
+
+func generateRepoPage(r *repo.Repo, out string, maxCommits int) error {
+ readme, err := r.GetReadme()
+ if err != nil {
+ return fmt.Errorf("get README: %w", err)
+ }
+
+ commits, err := r.GetCommits(maxCommits)
+ if err != nil {
+ return fmt.Errorf("get commits: %w", err)
+ }
+
+ files, err := r.GetFiles()
+ if err != nil {
+ return fmt.Errorf("get files: %w", err)
+ }
+
+ outPath := filepath.Join(out, "index.html")
+
+ return template.GenerateRepoPage(r.Name, readme, commits, files, outPa…
+}
+
+func contains(slice []string, item string) bool {
+ for _, a := range slice {
+ if a == item {
+ return true
+ }
+ }
+ return false
+}
diff --git a/internal/template/template.go b/internal/template/template.go
@@ -0,0 +1,94 @@
+package template
+
+import (
+ "embed"
+ "fmt"
+ "html/template"
+ "io"
+ "log"
+ "os"
+ "path/filepath"
+ "sort"
+
+ "staticgit/internal/repo"
+)
+
+//go:embed templates/*
+var templateFS embed.FS
+
+var (
+ templates = map[string]*template.Template{}
+)
+
+func init() {
+ for _, name := range []string{"index", "detail"} {
+ baseContent, err := templateFS.ReadFile("templates/base.html")
+ if err != nil {
+ log.Fatalf("Failed to read base.html: %v", err)
+ }
+
+ contentFile := fmt.Sprintf("templates/%s.html", name)
+ contentContent, err := templateFS.ReadFile(contentFile)
+ if err != nil {
+ log.Fatalf("Failed to read %s: %v", contentFile, err)
+ }
+
+ t, err := template.New(name).Parse(string(baseContent) + strin…
+ if err != nil {
+ log.Fatalf("Failed to parse %s template: %v", name, er…
+ }
+
+ templates[name] = t
+ }
+}
+
+func GenerateIndex(outDir string, repos []*repo.Repo) error {
+ sort.Slice(repos, func(i, j int) bool {
+ return repos[i].LastMod.After(repos[j].LastMod)
+ })
+
+ path := filepath.Join(outDir, "index.html")
+
+ f, err := os.Create(path)
+ if err != nil {
+ return fmt.Errorf("create index HTML: %w", err)
+ }
+ defer f.Close()
+
+ return executeTemplate("index", f, struct {
+ Title string
+ Repos []*repo.Repo
+ }{
+ Title: "Repos for days!",
+ Repos: repos,
+ })
+}
+
+func GenerateRepoPage(name, readmeContent string, commits []repo.Commit, files…
+ f, err := os.Create(outPath)
+ if err != nil {
+ return fmt.Errorf("create details HTML: %w", err)
+ }
+ defer f.Close()
+
+ return executeTemplate("detail", f, struct {
+ Title string
+ ReadmeContent string
+ Files []string
+ Commits []repo.Commit
+ }{
+ Title: "git clone git://jay.scot/" + name,
+ ReadmeContent: readmeContent,
+ Files: files,
+ Commits: commits,
+ })
+}
+
+func executeTemplate(name string, w io.Writer, data interface{}) error {
+ t, ok := templates[name]
+ if !ok {
+ return fmt.Errorf("template %s not found", name)
+ }
+
+ return t.Execute(w, data)
+}
diff --git a/internal/template/templates/base.html b/internal/template/template…
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>{{.Title}}</title>
+ <style>
+ body {
+ font-family: monospace;
+ line-height: 1.6;
+ margin: 0;
+ padding: 20px;
+ max-width: 1200px;
+ margin: 0 auto;
+ background-color: #f4f4f4;
+ }
+ h1, h2 { margin: 0.5em 0; }
+ table {
+ border-collapse: collapse;
+ width: 100%;
+ }
+ td, th {
+ border-bottom: 1px solid #ccc;
+ padding: 3px;
+ text-align: left;
+ }
+ @media (max-width: 600px) {
+ body { padding: 10px; }
+ table { font-size: 14px; }
+ }
+ </style>
+</head>
+<body>
+ <h1>{{.Title}}</h1>
+ {{block "content" .}}{{end}}
+</body>
+</html>
diff --git a/internal/template/templates/detail.html b/internal/template/templa…
@@ -0,0 +1,36 @@
+{{define "content"}}
+<pre>{{.ReadmeContent}}</pre>
+
+<h2>Commit History</h2>
+<table>
+ <thead>
+ <tr>
+ <th>Date</th>
+ <th>Message</th>
+ <th>Author</th>
+ <th>Hash</th>
+ <th>Added</th>
+ <th>Deleted</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{range .Commits}}
+ <tr>
+ <td>{{.Date}}</td>
+ <td>{{.Msg}}</td>
+ <td>{{.Author}}</td>
+ <td>{{.Hash}}</td>
+ <td>+{{.Added}}</td>
+ <td>-{{.Removed}}</td>
+ </tr>
+ {{end}}
+ </tbody>
+</table>
+
+<h2>Files</h2>
+<ul>
+ {{range .Files}}
+ <li>{{.}}</li>
+ {{end}}
+</ul>
+{{end}}
diff --git a/internal/template/templates/index.html b/internal/template/templat…
@@ -0,0 +1,20 @@
+{{define "content"}}
+<table>
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Description</th>
+ <th>Last commit</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{range .Repos}}
+ <tr>
+ <td><a href="{{.Name}}/index.html">{{.Name}}</a></td>
+ <td>{{.Description}}</td>
+ <td>{{.LastMod.Format "2006-01-02 15:04:05"}}</td>
+ </tr>
+ {{end}}
+ </tbody>
+</table>
+{{end}}
diff --git a/main.go b/main.go
@@ -1,452 +0,0 @@
-package main
-
-import (
- "flag"
- "fmt"
- "html/template"
- "log"
- "os"
- "path/filepath"
- "sort"
- "strings"
- "sync"
- "time"
-
- "github.com/go-git/go-git/v5"
- "github.com/go-git/go-git/v5/plumbing/object"
-)
-
-type Commit struct {
- Hash string
- Author string
- Date string
- Msg string
- Added int
- Removed int
-}
-
-type Repo struct {
- Name string
- Desc string
- LastMod time.Time
-}
-
-const (
- baseHtml = `
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>{{.Title}}</title>
- <style>
- body {
- font-family: monospace;
- line-height: 1.6;
- margin: 0;
- padding: 20px;
- max-width: 1200px;
- margin: 0 auto;
- background-color: #f4f4f4;
- }
- h1, h2 { margin: 0.5em 0; }
- table {
- border-collapse: collapse;
- width: 100%;
- }
- td, th {
- border-bottom: 1px solid #ccc;
- padding: 3px;
- text-align: left;
- }
- @media (max-width: 600px) {
- body { padding: 10px; }
- table { font-size: 14px; }
- }
- </style>
-</head>
-<body>
- <h1>{{.Title}}</h1>
- {{template "content" .}}
-</body>
-</html>
-`
-
- detailHtml = `
-{{define "content"}}
- <pre>{{.ReadmeContent}}</pre>
-
- <h2>Commit History</h2>
- <table>
- <thead>
- <tr>
- <th>Date</th>
- <th>Message</th>
- <th>Author</th>
- <th>Hash</th>
- <th>Added</th>
- <th>Deleted</th>
- </tr>
- </thead>
- <tbody>
- {{range .Commits}}
- <tr>
- <td>{{.Date}}</td>
- <td>{{.Msg}}</td>
- <td>{{.Author}}</td>
- <td>{{.Hash}}</td>
- <td>+{{.Added}}</td>
- <td>-{{.Removed}}</td>
- </tr>
- {{end}}
- </tbody>
- </table>
-
- <h2>Files</h2>
- <ul>
- {{range .Files}}
- <li>{{.}}</li>
- {{end}}
- </ul>
-{{end}}
-`
-
- indexHtml = `
-{{define "content"}}
- <table>
- <thead>
- <tr>
- <th>Name</th>
- <th>Description</th>
- <th>Last commit</th>
- </tr>
- </thead>
- <tbody>
- {{range .Repos}}
- <tr>
- <td><a href="{{.Name}}/index.html">{{.Name}}</a></td>
- <td>{{.Desc}}</td>
- <td>{{.LastMod.Format "2006-01-02 15:04:05"}}</td>
- </tr>
- {{end}}
- </tbody>
- </table>
-{{end}}
-`
-)
-
-var (
- templates = map[string]*template.Template{
- "index": template.Must(template.New("base").Parse(baseHtml +…
- "details": template.Must(template.New("base").Parse(baseHtml +…
- }
-
- repoDir string
- ignoreDirs map[string]bool
- outDir string
- maxCommits int
-)
-
-func genIndex(repos []Repo) error {
- sort.Slice(repos, func(i, j int) bool {
- return repos[i].LastMod.After(repos[j].LastMod)
- })
-
- path := filepath.Join(outDir, "index.html")
-
- f, err := os.Create(path)
- if err != nil {
- return fmt.Errorf("create index HTML: %w", err)
- }
- defer f.Close()
-
- return templates["index"].Execute(f, struct {
- Title string
- Repos []Repo
- }{
- Title: "Repos for days!",
- Repos: repos,
- })
-}
-
-func genRepo(name, path, out string) error {
- repo, err := git.PlainOpen(path)
- if err != nil {
- return fmt.Errorf("open git repo: %w", err)
- }
-
- readme, err := readme(path)
- if err != nil {
- return fmt.Errorf("get README: %w", err)
- }
-
- cs, err := commits(repo)
- if err != nil {
- return fmt.Errorf("get commits: %w", err)
- }
-
- fs, err := files(repo)
- if err != nil {
- return fmt.Errorf("get files: %w", err)
- }
-
- path = filepath.Join(out, "index.html")
-
- f, err := os.Create(path)
- if err != nil {
- return fmt.Errorf("create details HTML: %w", err)
- }
- defer f.Close()
-
- return templates["details"].Execute(f, struct {
- Title string
- ReadmeContent string
- Files []string
- Commits []Commit
- }{
- Title: "git clone git://jay.scot/" + name,
- ReadmeContent: readme,
- Files: fs,
- Commits: cs,
- })
-}
-
-func commits(repo *git.Repository) ([]Commit, error) {
- iter, err := repo.CommitObjects()
- if err != nil {
- return nil, fmt.Errorf("get commit objects: %w", err)
- }
-
- var cs []Commit
- count := 0
- limit := false
-
- err = iter.ForEach(func(c *object.Commit) error {
- if limit {
- return nil
- }
-
- stats, err := c.Stats()
- if err != nil {
- return fmt.Errorf("get commit stats: %w", err)
- }
-
- add, del := 0, 0
- for _, stat := range stats {
- add += stat.Addition
- del += stat.Deletion
- }
-
- cs = append(cs, Commit{
- Hash: c.Hash.String()[:7],
- Author: c.Author.Name,
- Date: c.Author.When.Format("02 Jan 2006 15:04:05"),
- Msg: strings.Split(c.Message, "\n")[0],
- Added: add,
- Removed: del,
- })
-
- count++
- if count >= maxCommits {
- limit = true
- }
- return nil
- })
-
- if err != nil {
- return nil, fmt.Errorf("iterate commits: %w", err)
- }
-
- sort.Slice(cs, func(i, j int) bool {
- timeI, _ := time.Parse("02 Jan 2006 15:04:05", cs[i].Date)
- timeJ, _ := time.Parse("02 Jan 2006 15:04:05", cs[j].Date)
- return timeI.After(timeJ)
- })
-
- return cs, nil
-}
-
-func files(repo *git.Repository) ([]string, error) {
- ref, err := repo.Head()
- if err != nil {
- return nil, fmt.Errorf("get HEAD: %w", err)
- }
-
- commit, err := repo.CommitObject(ref.Hash())
- if err != nil {
- return nil, fmt.Errorf("get commit object: %w", err)
- }
-
- tree, err := commit.Tree()
- if err != nil {
- return nil, fmt.Errorf("get tree: %w", err)
- }
-
- var fs []string
- err = tree.Files().ForEach(func(f *object.File) error {
- fs = append(fs, f.Name)
- return nil
- })
- if err != nil {
- return nil, fmt.Errorf("iterate files: %w", err)
- }
-
- sort.Strings(fs)
- return fs, nil
-}
-
-func readme(path string) (string, error) {
- names := []string{"README.md", "README.txt", "README"}
- repo, err := git.PlainOpen(path)
- if err != nil {
- return "", fmt.Errorf("open git repo: %w", err)
- }
-
- ref, err := repo.Head()
- if err != nil {
- return "", fmt.Errorf("get HEAD: %w", err)
- }
-
- commit, err := repo.CommitObject(ref.Hash())
- if err != nil {
- return "", fmt.Errorf("get commit object: %w", err)
- }
-
- tree, err := commit.Tree()
- if err != nil {
- return "", fmt.Errorf("get tree: %w", err)
- }
-
- for _, name := range names {
- file, err := tree.File(name)
- if err != nil {
- continue
- }
-
- content, err := file.Contents()
- if err != nil {
- return "", fmt.Errorf("read file contents: %w", err)
- }
-
- return content, nil
- }
-
- return "No README found!", nil
-}
-
-func repoInfo(path string) (Repo, error) {
- repo, err := git.PlainOpen(path)
- if err != nil {
- return Repo{}, fmt.Errorf("open repo: %w", err)
- }
-
- ref, err := repo.Head()
- if err != nil {
- return Repo{}, fmt.Errorf("get HEAD: %w", err)
- }
-
- commit, err := repo.CommitObject(ref.Hash())
- if err != nil {
- return Repo{}, fmt.Errorf("get commit object: %w", err)
- }
-
- content, _ := os.ReadFile(filepath.Join(path, "description"))
-
- return Repo{
- Name: filepath.Base(path),
- Desc: strings.TrimSpace(string(content)),
- LastMod: commit.Committer.When,
- }, nil
-}
-
-func ignoreList(dirs string) map[string]bool {
- ignore := make(map[string]bool)
- for _, dir := range strings.Split(dirs, ",") {
- if d := strings.TrimSpace(dir); d != "" {
- ignore[d] = true
- }
- }
- return ignore
-}
-
-func build() error {
- dirs, err := os.ReadDir(repoDir)
- if err != nil {
- return fmt.Errorf("read repos dir: %w", err)
- }
-
- var wg sync.WaitGroup
- repoChan := make(chan Repo, len(dirs))
- errChan := make(chan error, len(dirs))
-
- for _, d := range dirs {
- if d.IsDir() && !ignoreDirs[d.Name()] {
- wg.Add(1)
- go func(d os.DirEntry) {
- defer wg.Done()
- path := filepath.Join(repoDir, d.Name())
- repo, err := repoInfo(path)
- if err != nil {
- errChan <- fmt.Errorf("get info for %s…
- return
- }
- repoChan <- repo
-
- out := filepath.Join(outDir, d.Name())
- if err := os.MkdirAll(out, 0755); err != nil {
- errChan <- fmt.Errorf("create dir for …
- return
- }
-
- if err := genRepo(d.Name(), path, out); err !=…
- errChan <- fmt.Errorf("process %s: %w"…
- }
- }(d)
- }
- }
-
- go func() {
- wg.Wait()
- close(repoChan)
- close(errChan)
- }()
-
- var repos []Repo
- for repo := range repoChan {
- repos = append(repos, repo)
- }
-
- for err := range errChan {
- fmt.Println(err)
- }
-
- return genIndex(repos)
-}
-
-func main() {
- flag.StringVar(&repoDir, "p", "", "Path to git repos (required)")
- flag.StringVar(&outDir, "o", ".", "Root path for output")
- flag.IntVar(&maxCommits, "c", 100, "Max commits to display (default 10…
- ignore := flag.String("i", "", "Dirs to ignore (comma-separated)")
-
- flag.Usage = func() {
- fmt.Fprintf(os.Stderr, "Usage: %s [options]\n\n", os.Args[0])
- fmt.Fprintf(os.Stderr, "Options:\n")
- flag.PrintDefaults()
- fmt.Fprintf(os.Stderr, "\nExample:\n")
- fmt.Fprintf(os.Stderr, " %s -p /path/to/repos -i dir1,dir2 -o…
- }
-
- flag.Parse()
-
- if repoDir == "" {
- flag.Usage()
- os.Exit(1)
- }
-
- ignoreDirs = ignoreList(*ignore)
-
- if err := build(); err != nil {
- log.Fatalf("Error building site: %v", err)
- }
-}
You are viewing proxied material from jay.scot. The copyright of proxied material belongs to its original authors. Any comments or complaints in relation to proxied material should be directed to the original authors of the content concerned. Please see the disclaimer for more details.