Commit 1bbe5e4
Changed files (8)
cmd/gitmal/main.go
@@ -1,6 +1,7 @@
package main
import (
+ "errors"
"fmt"
"os"
"path/filepath"
@@ -15,6 +16,13 @@ import (
var flagOutput string
func main() {
+ if err := run(); err != nil {
+ fmt.Fprintln(os.Stderr, "error:", err)
+ os.Exit(1)
+ }
+}
+
+func run() error {
flag.StringVar(&flagOutput, "output", "output", "Output directory")
flag.Usage = usage
flag.Parse()
@@ -25,17 +33,17 @@ func main() {
input = args[0]
}
if len(args) > 1 {
- panic("Multiple repos not supported yet")
+ return errors.New("multiple repos not supported yet")
}
outputDir, err := filepath.Abs(flagOutput)
if err != nil {
- panic(err)
+ return err
}
absInput, err := filepath.Abs(input)
if err != nil {
- panic(err)
+ return err
}
input = absInput
@@ -44,12 +52,12 @@ func main() {
branches, err := git.Branches(input)
if err != nil {
- panic(err)
+ return err
}
tags, err := git.Tags(input)
if err != nil {
- panic(err)
+ return err
}
var defaultBranch string
@@ -58,13 +66,11 @@ func main() {
} else if generator.ContainsBranch(branches, "main") {
defaultBranch = "main"
} else {
- generator.Echo("No default branch found (expected 'main' or 'master').")
- os.Exit(1)
+ return errors.New("no default branch found (expected 'main' or 'master')")
}
if yes, a, b := generator.HasConflictingBranchNames(branches); yes {
- generator.Echo(fmt.Sprintf("Conflicting branchs %q and %q, both want to use %q dir name.", a, b, a.DirName()))
- os.Exit(1)
+ return fmt.Errorf("conflicting branches %q and %q, both want to use %q dir name", a, b, a.DirName())
}
params := generator.Params{
@@ -80,7 +86,7 @@ func main() {
for _, branch := range branches {
commitsFor[branch], err = git.Commits(branch, params.RepoDir)
if err != nil {
- panic(err)
+ return err
}
for _, commit := range commitsFor[branch] {
@@ -95,7 +101,7 @@ func main() {
for _, tag := range tags {
commitsForTag, err := git.Commits(git.NewRef(tag.Name), params.RepoDir)
if err != nil {
- panic(err)
+ return err
}
for _, commit := range commitsForTag {
if alreadyExisting, ok := commits[commit.Hash]; ok && !alreadyExisting.Branch.IsEmpty() {
@@ -108,15 +114,15 @@ func main() {
generator.Echo(fmt.Sprintf("> %s: %d branches, %d tags, %d commits", params.Name, len(branches), len(tags), len(commits)))
if err := generator.GenerateCSS(params); err != nil {
- panic(err)
+ return err
}
if err := generator.GenerateBranches(branches, defaultBranch, params); err != nil {
- panic(err)
+ return err
}
if err := generator.GenerateBranchesJSON(branches, commitsFor, params); err != nil {
- panic(err)
+ return err
}
var defaultBranchFiles []git.Blob
@@ -127,7 +133,7 @@ func main() {
files, err := git.Files(params.Ref, params.RepoDir)
if err != nil {
- panic(err)
+ return err
}
if branch.String() == defaultBranch {
@@ -135,23 +141,23 @@ func main() {
}
if err := generator.GenerateBlobs(files, params); err != nil {
- panic(err)
+ return err
}
if err := generator.GenerateLists(files, params); err != nil {
- panic(err)
+ return err
}
if err := generator.GenerateLogForBranch(commitsFor[branch], params); err != nil {
- panic(err)
+ return err
}
if err := generator.GenerateCommitsJSON(commitsFor[branch], params); err != nil {
- panic(err)
+ return err
}
if err := generator.GenerateCommitsAtom(commitsFor[branch], params); err != nil {
- panic(err)
+ return err
}
}
@@ -159,28 +165,29 @@ func main() {
generator.Echo("> generating commits...")
if err := generator.GenerateCommits(commits, params); err != nil {
- panic(err)
+ return err
}
if err := generator.GenerateTags(tags, params); err != nil {
- panic(err)
+ return err
}
if err := generator.GenerateTagsAtom(tags, params); err != nil {
- panic(err)
+ return err
}
if err := generator.GenerateReleasesAtom(tags, params); err != nil {
- panic(err)
+ return err
}
if len(defaultBranchFiles) == 0 {
- panic("No files found for default branch")
+ return errors.New("no files found for default branch")
}
if err := generator.GenerateIndex(defaultBranchFiles, params); err != nil {
- panic(err)
+ return err
}
+ return nil
}
func usage() {
internal/generator/blob.go
@@ -8,11 +8,9 @@ import (
"path/filepath"
"strings"
- "github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
- "github.com/yuin/goldmark"
"mokhan.ca/antonmedv/gitmal/internal/git"
"mokhan.ca/antonmedv/gitmal/internal/links"
@@ -20,12 +18,6 @@ import (
"mokhan.ca/antonmedv/gitmal/internal/templates"
)
-type blobWorker struct {
- md goldmark.Markdown
- formatter *html.Formatter
- style *chroma.Style
-}
-
func GenerateBlobs(files []git.Blob, params Params) error {
formatterOptions := []html.Option{
html.WithLineNumbers(true),
@@ -34,134 +26,129 @@ func GenerateBlobs(files []git.Blob, params Params) error {
html.WithCSSComments(false),
}
+ md := createMarkdown()
+ formatter := html.New(formatterOptions...)
+ style := styles.Get("github")
+
dirsSet := links.BuildDirSet(files)
filesSet := links.BuildFileSet(files)
- return pool.RunWithInit(files,
- func() *blobWorker {
- return &blobWorker{
- md: createMarkdown(),
- formatter: html.New(formatterOptions...),
- style: styles.Get("github"),
- }
- },
- func(w *blobWorker, blob git.Blob) error {
- var content string
- data, isBin, err := git.BlobContent(params.Ref, blob.Path, params.RepoDir)
- if err != nil {
+ return pool.Run(files, func(blob git.Blob) error {
+ var content string
+ data, isBin, err := git.BlobContent(params.Ref, blob.Path, params.RepoDir)
+ if err != nil {
+ return err
+ }
+
+ isImg := isImage(blob.Path)
+ if !isBin {
+ content = string(data)
+ }
+
+ outPath := filepath.Join(params.OutputDir, "blob", params.Ref.DirName(), blob.Path) + ".html"
+ if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil {
+ return err
+ }
+
+ f, err := os.Create(outPath)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ depth := 0
+ if strings.Contains(blob.Path, "/") {
+ depth = len(strings.Split(blob.Path, "/")) - 1
+ }
+ rootHref := strings.Repeat("../", depth+2)
+
+ if isMarkdown(blob.Path) {
+ var b bytes.Buffer
+ if err := md.Convert([]byte(content), &b); err != nil {
return err
}
- isImg := isImage(blob.Path)
- if !isBin {
- content = string(data)
- }
+ contentHTML := links.Resolve(
+ b.String(),
+ blob.Path,
+ rootHref,
+ params.Ref.DirName(),
+ dirsSet,
+ filesSet,
+ )
- outPath := filepath.Join(params.OutputDir, "blob", params.Ref.DirName(), blob.Path) + ".html"
- if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil {
+ return templates.MarkdownTemplate.ExecuteTemplate(f, "layout.gohtml", templates.MarkdownParams{
+ LayoutParams: templates.LayoutParams{
+ Title: fmt.Sprintf("%s/%s at %s", params.Name, blob.Path, params.Ref),
+ Name: params.Name,
+ RootHref: rootHref,
+ CurrentRefDir: params.Ref.DirName(),
+ Selected: "code",
+ NeedsMarkdownCSS: true,
+ NeedsSyntaxCSS: true,
+ },
+ HeaderParams: templates.HeaderParams{
+ Ref: params.Ref,
+ Breadcrumbs: breadcrumbs(params.Name, blob.Path, true),
+ },
+ Blob: blob,
+ Content: template.HTML(contentHTML),
+ })
+ }
+
+ var contentHTML template.HTML
+ if !isBin {
+ var b bytes.Buffer
+ lx := lexers.Match(blob.Path)
+ if lx == nil {
+ lx = lexers.Fallback
+ }
+ iterator, _ := lx.Tokenise(nil, content)
+ if err := formatter.Format(&b, style, iterator); err != nil {
+ return err
+ }
+ contentHTML = template.HTML(b.String())
+ } else if isImg {
+ rawPath := filepath.Join(params.OutputDir, "raw", params.Ref.DirName(), blob.Path)
+ if err := os.MkdirAll(filepath.Dir(rawPath), 0o755); err != nil {
return err
}
- f, err := os.Create(outPath)
+ rf, err := os.Create(rawPath)
if err != nil {
return err
}
- defer f.Close()
- depth := 0
- if strings.Contains(blob.Path, "/") {
- depth = len(strings.Split(blob.Path, "/")) - 1
- }
- rootHref := strings.Repeat("../", depth+2)
-
- if isMarkdown(blob.Path) {
- var b bytes.Buffer
- if err := w.md.Convert([]byte(content), &b); err != nil {
- return err
- }
-
- contentHTML := links.Resolve(
- b.String(),
- blob.Path,
- rootHref,
- params.Ref.DirName(),
- dirsSet,
- filesSet,
- )
-
- return templates.MarkdownTemplate.ExecuteTemplate(f, "layout.gohtml", templates.MarkdownParams{
- LayoutParams: templates.LayoutParams{
- Title: fmt.Sprintf("%s/%s at %s", params.Name, blob.Path, params.Ref),
- Name: params.Name,
- RootHref: rootHref,
- CurrentRefDir: params.Ref.DirName(),
- Selected: "code",
- NeedsMarkdownCSS: true,
- NeedsSyntaxCSS: true,
- },
- HeaderParams: templates.HeaderParams{
- Ref: params.Ref,
- Breadcrumbs: breadcrumbs(params.Name, blob.Path, true),
- },
- Blob: blob,
- Content: template.HTML(contentHTML),
- })
+ if _, err := rf.Write(data); err != nil {
+ rf.Close()
+ return err
}
-
- var contentHTML template.HTML
- if !isBin {
- var b bytes.Buffer
- lx := lexers.Match(blob.Path)
- if lx == nil {
- lx = lexers.Fallback
- }
- iterator, _ := lx.Tokenise(nil, content)
- if err := w.formatter.Format(&b, w.style, iterator); err != nil {
- return err
- }
- contentHTML = template.HTML(b.String())
- } else if isImg {
- rawPath := filepath.Join(params.OutputDir, "raw", params.Ref.DirName(), blob.Path)
- if err := os.MkdirAll(filepath.Dir(rawPath), 0o755); err != nil {
- return err
- }
-
- rf, err := os.Create(rawPath)
- if err != nil {
- return err
- }
-
- if _, err := rf.Write(data); err != nil {
- rf.Close()
- return err
- }
- if err := rf.Close(); err != nil {
- return err
- }
-
- relativeRawPath := filepath.Join(rootHref, "raw", params.Ref.DirName(), blob.Path)
- contentHTML = template.HTML(fmt.Sprintf(`<img src="%s" alt="%s" />`, relativeRawPath, blob.FileName))
+ if err := rf.Close(); err != nil {
+ return err
}
- return templates.BlobTemplate.ExecuteTemplate(f, "layout.gohtml", templates.BlobParams{
- LayoutParams: templates.LayoutParams{
- Title: fmt.Sprintf("%s/%s at %s", params.Name, blob.Path, params.Ref),
- Name: params.Name,
- RootHref: rootHref,
- CurrentRefDir: params.Ref.DirName(),
- Selected: "code",
- NeedsSyntaxCSS: !isBin,
- },
- HeaderParams: templates.HeaderParams{
- Ref: params.Ref,
- Breadcrumbs: breadcrumbs(params.Name, blob.Path, true),
- },
- Blob: blob,
- IsBinary: isBin,
- IsImage: isImg,
- Content: contentHTML,
- })
- },
- )
+ relativeRawPath := filepath.Join(rootHref, "raw", params.Ref.DirName(), blob.Path)
+ contentHTML = template.HTML(fmt.Sprintf(`<img src="%s" alt="%s" />`, relativeRawPath, blob.FileName))
+ }
+
+ return templates.BlobTemplate.ExecuteTemplate(f, "layout.gohtml", templates.BlobParams{
+ LayoutParams: templates.LayoutParams{
+ Title: fmt.Sprintf("%s/%s at %s", params.Name, blob.Path, params.Ref),
+ Name: params.Name,
+ RootHref: rootHref,
+ CurrentRefDir: params.Ref.DirName(),
+ Selected: "code",
+ NeedsSyntaxCSS: !isBin,
+ },
+ HeaderParams: templates.HeaderParams{
+ Ref: params.Ref,
+ Breadcrumbs: breadcrumbs(params.Name, blob.Path, true),
+ },
+ Blob: blob,
+ IsBinary: isBin,
+ IsImage: isImg,
+ Content: contentHTML,
+ })
+ })
}
internal/generator/list.go
@@ -19,34 +19,28 @@ type dirInfo struct {
}
func buildDirTree(files []git.Blob) map[string]*dirInfo {
- dirs := map[string]*dirInfo{}
-
- ensureDir := func(p string) *dirInfo {
- if di, ok := dirs[p]; ok {
- return di
- }
- di := &dirInfo{subdirs: map[string]struct{}{}, files: []git.Blob{}}
- dirs[p] = di
- return di
- }
+ dirs := make(map[string]*dirInfo)
+ dirs[""] = &dirInfo{subdirs: make(map[string]struct{})}
for _, b := range files {
- p := b.Path
- parts := strings.Split(p, "/")
+ parts := strings.Split(b.Path, "/")
cur := ""
for i := 0; i < len(parts)-1; i++ {
- child := parts[i]
- ensureDir(cur).subdirs[child] = struct{}{}
+ if dirs[cur] == nil {
+ dirs[cur] = &dirInfo{subdirs: make(map[string]struct{})}
+ }
+ dirs[cur].subdirs[parts[i]] = struct{}{}
if cur == "" {
- cur = child
+ cur = parts[i]
} else {
- cur = cur + "/" + child
+ cur += "/" + parts[i]
}
- ensureDir(cur)
}
- ensureDir(cur).files = append(ensureDir(cur).files, b)
+ if dirs[cur] == nil {
+ dirs[cur] = &dirInfo{subdirs: make(map[string]struct{})}
+ }
+ dirs[cur].files = append(dirs[cur].files, b)
}
-
return dirs
}
internal/git/git.go
@@ -11,14 +11,25 @@ import (
"time"
)
-func Branches(repoDir string) ([]Ref, error) {
- cmd := exec.Command("git", "for-each-ref", "--format=%(refname:short)", "refs/heads/")
+func gitCmd(repoDir string, args ...string) ([]byte, error) {
+ cmd := exec.Command("git", args...)
if repoDir != "" {
cmd.Dir = repoDir
}
out, err := cmd.Output()
if err != nil {
- return nil, fmt.Errorf("failed to list branches: %w", err)
+ if ee, ok := err.(*exec.ExitError); ok {
+ return nil, fmt.Errorf("git %s: %w: %s", args[0], err, ee.Stderr)
+ }
+ return nil, err
+ }
+ return out, nil
+}
+
+func Branches(repoDir string) ([]Ref, error) {
+ out, err := gitCmd(repoDir, "for-each-ref", "--format=%(refname:short)", "refs/heads/")
+ if err != nil {
+ return nil, err
}
lines := strings.Split(string(out), "\n")
branches := make([]Ref, 0, len(lines))
@@ -33,24 +44,19 @@ func Branches(repoDir string) ([]Ref, error) {
func Tags(repoDir string) ([]Tag, error) {
format := []string{
- "%(refname:short)", // tag name
- "%(creatordate:unix)", // creation date
- "%(objectname)", // commit hash for lightweight tags
- "%(*objectname)", // peeled object => commit hash
+ "%(refname:short)",
+ "%(creatordate:unix)",
+ "%(objectname)",
+ "%(*objectname)",
}
- args := []string{
+ out, err := gitCmd(repoDir,
"for-each-ref",
"--sort=-creatordate",
- "--format=" + strings.Join(format, "%00"),
+ "--format="+strings.Join(format, "%00"),
"refs/tags",
- }
- cmd := exec.Command("git", args...)
- if repoDir != "" {
- cmd.Dir = repoDir
- }
- out, err := cmd.Output()
+ )
if err != nil {
- return nil, fmt.Errorf("failed to list tags: %w", err)
+ return nil, err
}
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
@@ -194,52 +200,22 @@ func BlobContent(ref Ref, path string, repoDir string) ([]byte, bool, error) {
if ref.IsEmpty() {
ref = NewRef("HEAD")
}
- // Use `git show ref:path` to get the blob content at that ref
- cmd := exec.Command("git", "show", ref.String()+":"+path)
- if repoDir != "" {
- cmd.Dir = repoDir
- }
- out, err := cmd.Output()
+ out, err := gitCmd(repoDir, "show", ref.String()+":"+path)
if err != nil {
- // include stderr if available
- if ee, ok := err.(*exec.ExitError); ok {
- return nil, false, fmt.Errorf("git show failed: %v: %s", err, string(ee.Stderr))
- }
- return nil, false, fmt.Errorf("git show failed: %w", err)
+ return nil, false, err
}
return out, IsBinary(out), nil
}
func Commits(ref Ref, repoDir string) ([]Commit, error) {
- format := []string{
- "%H", // commit hash
- "%h", // abbreviated commit hash
- "%s", // subject
- "%b", // body
- "%an", // author name
- "%ae", // author email
- "%ad", // author date
- "%cn", // committer name
- "%ce", // committer email
- "%cd", // committer date
- "%P", // parent hashes
- "%D", // ref names without the "(", ")" wrapping.
- }
-
- args := []string{
+ format := []string{"%H", "%h", "%s", "%b", "%an", "%ae", "%ad", "%cn", "%ce", "%cd", "%P", "%D"}
+ out, err := gitCmd(repoDir,
"log",
"--date=unix",
- "--pretty=format:" + strings.Join(format, "\x1F"),
- "-z", // Separate the commits with NULs instead of newlines
+ "--pretty=format:"+strings.Join(format, "\x1F"),
+ "-z",
ref.String(),
- }
-
- cmd := exec.Command("git", args...)
- if repoDir != "" {
- cmd.Dir = repoDir
- }
-
- out, err := cmd.Output()
+ )
if err != nil {
return nil, err
}
@@ -347,12 +323,7 @@ func parseRefNames(refNames string) []RefName {
}
func CommitDiff(hash, repoDir string) (string, error) {
- // unified diff without a commit header
- cmd := exec.Command("git", "show", "--pretty=format:", "--patch", hash)
- if repoDir != "" {
- cmd.Dir = repoDir
- }
- out, err := cmd.Output()
+ out, err := gitCmd(repoDir, "show", "--pretty=format:", "--patch", hash)
if err != nil {
return "", err
}
internal/pool/pool.go
@@ -1,83 +1,19 @@
package pool
import (
- "context"
"runtime"
- "sync"
+
+ "golang.org/x/sync/errgroup"
)
func Run[T any](items []T, fn func(T) error) error {
- return RunWithInit(items, func() struct{} { return struct{}{} }, func(_ struct{}, item T) error {
- return fn(item)
- })
-}
-
-func RunWithInit[T, W any](items []T, init func() W, fn func(W, T) error) error {
if len(items) == 0 {
return nil
}
-
- workers := runtime.NumCPU()
- if workers < 1 {
- workers = 1
+ g := &errgroup.Group{}
+ g.SetLimit(runtime.NumCPU())
+ for _, item := range items {
+ g.Go(func() error { return fn(item) })
}
-
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
-
- jobs := make(chan T)
- errCh := make(chan error, 1)
- var wg sync.WaitGroup
-
- wg.Add(workers)
- for i := 0; i < workers; i++ {
- go func() {
- defer wg.Done()
- w := init()
- for {
- select {
- case <-ctx.Done():
- return
- case item, ok := <-jobs:
- if !ok {
- return
- }
- if err := fn(w, item); err != nil {
- select {
- case errCh <- err:
- cancel()
- default:
- }
- return
- }
- }
- }
- }()
- }
-
- go func() {
- defer close(jobs)
- for _, item := range items {
- select {
- case <-ctx.Done():
- return
- case jobs <- item:
- }
- }
- }()
-
- done := make(chan struct{})
- go func() {
- wg.Wait()
- close(done)
- }()
-
- var err error
- select {
- case err = <-errCh:
- <-done
- case <-done:
- }
-
- return err
+ return g.Wait()
}
go.mod
@@ -11,4 +11,7 @@ require (
golang.org/x/net v0.47.0
)
-require github.com/dlclark/regexp2 v1.11.5 // indirect
+require (
+ github.com/dlclark/regexp2 v1.11.5 // indirect
+ golang.org/x/sync v0.19.0 // indirect
+)
go.sum
@@ -28,5 +28,7 @@ github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
README.md
@@ -53,12 +53,6 @@ Generated paths follow GitHub repository conventions:
| `/releases.atom` | Releases Atom feed |
| `/branches.json` | Branches JSON |
-## Building
-
-```bash
-go build ./cmd/gitmal
-```
-
## License
MIT