diff options
| author | mo khan <mo@mokhan.ca> | 2026-01-30 18:04:23 -0700 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2026-01-30 18:04:23 -0700 |
| commit | d707ce9185089e968a5b826058bb530472daae13 (patch) | |
| tree | 07c34bab6298f3a9d5a5cfc9af4774ed27596deb | |
| parent | ff213f4113434fee4ddba1f0874d3c2c99b4f9de (diff) | |
refactor: move main.go to cmd/gitmal/
| -rw-r--r-- | Makefile | 4 | ||||
| -rw-r--r-- | blob.go | 259 | ||||
| -rw-r--r-- | branches.go | 65 | ||||
| -rw-r--r-- | branches_json.go | 43 | ||||
| -rw-r--r-- | commit.go | 335 | ||||
| -rw-r--r-- | commits_atom.go | 96 | ||||
| -rw-r--r-- | commits_json.go | 71 | ||||
| -rw-r--r-- | commits_list.go | 101 | ||||
| -rw-r--r-- | index.go | 129 | ||||
| -rw-r--r-- | list.go | 249 | ||||
| -rw-r--r-- | main.go | 289 | ||||
| -rw-r--r-- | markdown.go | 38 | ||||
| -rw-r--r-- | post_process.go | 139 | ||||
| -rw-r--r-- | readme.go | 45 | ||||
| -rw-r--r-- | tags.go | 37 | ||||
| -rw-r--r-- | tags_atom.go | 111 | ||||
| -rw-r--r-- | themes.go | 156 | ||||
| -rw-r--r-- | utils.go | 126 |
18 files changed, 2 insertions, 2291 deletions
@@ -1,7 +1,7 @@ .PHONY: build test clean run release build: - go build -o gitmal . + go build -o gitmal ./cmd/gitmal test: go test ./... @@ -13,4 +13,4 @@ clean: rm -rf gitmal output release: - CGO_ENABLED=0 go build -o gitmal . + CGO_ENABLED=0 go build -o gitmal ./cmd/gitmal diff --git a/blob.go b/blob.go deleted file mode 100644 index 8b52283..0000000 --- a/blob.go +++ /dev/null @@ -1,259 +0,0 @@ -package main - -import ( - "bytes" - "context" - "fmt" - "html/template" - "os" - "path/filepath" - "runtime" - "strings" - "sync" - - "github.com/alecthomas/chroma/v2/formatters/html" - "github.com/alecthomas/chroma/v2/lexers" - "github.com/alecthomas/chroma/v2/styles" - - "mokhan.ca/antonmedv/gitmal/pkg/git" - "mokhan.ca/antonmedv/gitmal/pkg/links" - "mokhan.ca/antonmedv/gitmal/pkg/progress_bar" - "mokhan.ca/antonmedv/gitmal/pkg/templates" -) - -func generateBlobs(files []git.Blob, params Params) error { - // Prepare shared, read-only resources - var css strings.Builder - style := styles.Get(params.Style) - if style == nil { - return fmt.Errorf("unknown style: %s", params.Style) - } - - formatterOptions := []html.Option{ - html.WithLineNumbers(true), - html.WithLinkableLineNumbers(true, "L"), - html.WithClasses(true), - html.WithCSSComments(false), - } - - // Use a temporary formatter to render CSS once - if err := html.New(formatterOptions...).WriteCSS(&css, style); err != nil { - return err - } - - dirsSet := links.BuildDirSet(files) - filesSet := links.BuildFileSet(files) - - // Bounded worker pool - workers := runtime.NumCPU() - if workers < 1 { - workers = 1 - } - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - jobs := make(chan git.Blob) - errCh := make(chan error, 1) - var wg sync.WaitGroup - - p := progress_bar.NewProgressBar("blobs for "+params.Ref.String(), len(files)) - - workerFn := func() { - defer wg.Done() - - // Per-worker instances - md := createMarkdown(params.Style) - formatter := html.New(formatterOptions...) - - check := func(err error) bool { - if err != nil { - select { - case errCh <- err: - cancel() - default: - } - return true - } - return false - } - - for { - select { - case <-ctx.Done(): - return - case blob, ok := <-jobs: - if !ok { - return - } - func() { - var content string - data, isBin, err := git.BlobContent(params.Ref, blob.Path, params.RepoDir) - if check(err) { - return - } - - 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); check(err) { - return - } - - f, err := os.Create(outPath) - if check(err) { - return - } - defer func() { - _ = 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); check(err) { - return - } - - contentHTML := links.Resolve( - b.String(), - blob.Path, - rootHref, - params.Ref.DirName(), - dirsSet, - filesSet, - ) - - err = templates.MarkdownTemplate.ExecuteTemplate(f, "layout.gohtml", templates.MarkdownParams{ - LayoutParams: templates.LayoutParams{ - Title: fmt.Sprintf("%s/%s at %s", params.Name, blob.Path, params.Ref), - Dark: params.Dark, - CSSMarkdown: cssMarkdown(params.Dark), - Name: params.Name, - RootHref: rootHref, - CurrentRefDir: params.Ref.DirName(), - Selected: "code", - }, - HeaderParams: templates.HeaderParams{ - Ref: params.Ref, - Breadcrumbs: breadcrumbs(params.Name, blob.Path, true), - }, - Blob: blob, - Content: template.HTML(contentHTML), - }) - if check(err) { - return - } - - } else { - - 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); check(err) { - return - } - 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); check(err) { - return - } - - rf, err := os.Create(rawPath) - if check(err) { - return - } - defer func() { - _ = rf.Close() - }() - - if _, err := rf.Write(data); check(err) { - return - } - - relativeRawPath := filepath.Join(rootHref, "raw", params.Ref.DirName(), blob.Path) - contentHTML = template.HTML(fmt.Sprintf(`<img src="%s" alt="%s" />`, relativeRawPath, blob.FileName)) - } - - err = templates.BlobTemplate.ExecuteTemplate(f, "layout.gohtml", templates.BlobParams{ - LayoutParams: templates.LayoutParams{ - Title: fmt.Sprintf("%s/%s at %s", params.Name, blob.Path, params.Ref), - Dark: params.Dark, - Name: params.Name, - RootHref: rootHref, - CurrentRefDir: params.Ref.DirName(), - Selected: "code", - }, - HeaderParams: templates.HeaderParams{ - Ref: params.Ref, - Breadcrumbs: breadcrumbs(params.Name, blob.Path, true), - }, - CSS: template.CSS(css.String()), - Blob: blob, - IsBinary: isBin, - IsImage: isImg, - Content: contentHTML, - }) - if check(err) { - return - } - } - }() - - p.Inc() - } - } - } - - // Start workers - wg.Add(workers) - for i := 0; i < workers; i++ { - go workerFn() - } - - // Feed jobs - go func() { - defer close(jobs) - for _, b := range files { - select { - case <-ctx.Done(): - return - case jobs <- b: - } - } - }() - - // Wait for workers - doneCh := make(chan struct{}) - go func() { - wg.Wait() - close(doneCh) - }() - - var runErr error - select { - case runErr = <-errCh: - // error occurred, wait workers to finish - <-doneCh - case <-doneCh: - } - - p.Done() - return runErr -} diff --git a/branches.go b/branches.go deleted file mode 100644 index d574954..0000000 --- a/branches.go +++ /dev/null @@ -1,65 +0,0 @@ -package main - -import ( - "fmt" - "os" - "path/filepath" - "sort" - - "mokhan.ca/antonmedv/gitmal/pkg/git" - "mokhan.ca/antonmedv/gitmal/pkg/templates" -) - -// generateBranches creates a branches.html page at the root of the output -// that lists all branches and links to their root directory listings. -func generateBranches(branches []git.Ref, defaultBranch string, params Params) error { - outDir := params.OutputDir - if err := os.MkdirAll(outDir, 0o755); err != nil { - return err - } - - entries := make([]templates.BranchEntry, 0, len(branches)) - for _, b := range branches { - entries = append(entries, templates.BranchEntry{ - Name: b.String(), - Href: filepath.ToSlash(filepath.Join("blob", b.DirName()) + "/index.html"), - IsDefault: b.String() == defaultBranch, - CommitsHref: filepath.ToSlash(filepath.Join("commits", b.DirName(), "index.html")), - }) - } - - // Ensure default branch is shown at the top of the list. - // Keep remaining branches sorted alphabetically for determinism. - sort.SliceStable(entries, func(i, j int) bool { - if entries[i].IsDefault != entries[j].IsDefault { - return entries[i].IsDefault && !entries[j].IsDefault - } - return entries[i].Name < entries[j].Name - }) - - f, err := os.Create(filepath.Join(outDir, "branches.html")) - if err != nil { - return err - } - defer f.Close() - - // RootHref from root page is just ./ - rootHref := "./" - - err = templates.BranchesTemplate.ExecuteTemplate(f, "layout.gohtml", templates.BranchesParams{ - LayoutParams: templates.LayoutParams{ - Title: fmt.Sprintf("Branches %s %s", dot, params.Name), - Name: params.Name, - Dark: params.Dark, - RootHref: rootHref, - CurrentRefDir: params.DefaultRef.DirName(), - Selected: "branches", - }, - Branches: entries, - }) - if err != nil { - return err - } - - return nil -} diff --git a/branches_json.go b/branches_json.go deleted file mode 100644 index d967484..0000000 --- a/branches_json.go +++ /dev/null @@ -1,43 +0,0 @@ -package main - -import ( - "encoding/json" - "os" - "path/filepath" - - "mokhan.ca/antonmedv/gitmal/pkg/git" -) - -type BranchJSON struct { - Name string `json:"name"` - Commit BranchHead `json:"commit"` -} - -type BranchHead struct { - SHA string `json:"sha"` -} - -func generateBranchesJSON(branches []git.Ref, commitsFor map[git.Ref][]git.Commit, params Params) error { - list := make([]BranchJSON, 0, len(branches)) - for _, branch := range branches { - commits := commitsFor[branch] - var sha string - if len(commits) > 0 { - sha = commits[0].Hash - } - list = append(list, BranchJSON{ - Name: branch.String(), - Commit: BranchHead{SHA: sha}, - }) - } - - outPath := filepath.Join(params.OutputDir, "branches.json") - f, err := os.Create(outPath) - if err != nil { - return err - } - defer f.Close() - encoder := json.NewEncoder(f) - encoder.SetIndent("", " ") - return encoder.Encode(list) -} diff --git a/commit.go b/commit.go deleted file mode 100644 index 442b09c..0000000 --- a/commit.go +++ /dev/null @@ -1,335 +0,0 @@ -package main - -import ( - "bytes" - "context" - "fmt" - "html/template" - "os" - "path/filepath" - "runtime" - "sort" - "strings" - "sync" - - "github.com/alecthomas/chroma/v2" - "github.com/alecthomas/chroma/v2/formatters/html" - "github.com/alecthomas/chroma/v2/lexers" - "github.com/alecthomas/chroma/v2/styles" - - "mokhan.ca/antonmedv/gitmal/pkg/git" - "mokhan.ca/antonmedv/gitmal/pkg/gitdiff" - "mokhan.ca/antonmedv/gitmal/pkg/progress_bar" - "mokhan.ca/antonmedv/gitmal/pkg/templates" -) - -func generateCommits(commits map[string]git.Commit, params Params) error { - outDir := filepath.Join(params.OutputDir, "commit") - if err := os.MkdirAll(outDir, 0o755); err != nil { - return err - } - - list := make([]git.Commit, 0, len(commits)) - for _, c := range commits { - list = append(list, c) - } - - workers := runtime.NumCPU() - if workers < 1 { - workers = 1 - } - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - jobs := make(chan git.Commit) - errCh := make(chan error, 1) - var wg sync.WaitGroup - - p := progress_bar.NewProgressBar("commits", len(list)) - - workerFn := func() { - defer wg.Done() - for { - select { - case <-ctx.Done(): - return - case c, ok := <-jobs: - if !ok { - return - } - if err := generateCommitPage(c, params); err != nil { - select { - case errCh <- err: - cancel() - default: - } - return - } - p.Inc() - } - } - } - - wg.Add(workers) - for i := 0; i < workers; i++ { - go workerFn() - } - - go func() { - defer close(jobs) - for _, c := range list { - select { - case <-ctx.Done(): - return - case jobs <- c: - } - } - }() - - done := make(chan struct{}) - go func() { - wg.Wait() - close(done) - }() - - var err error - select { - case err = <-errCh: - cancel() - <-done - case <-done: - } - - p.Done() - return err -} - -func generateCommitPage(commit git.Commit, params Params) error { - diff, err := git.CommitDiff(commit.Hash, params.RepoDir) - if err != nil { - return err - } - - files, _, err := gitdiff.Parse(strings.NewReader(diff)) - if err != nil { - return err - } - - style := styles.Get(params.Style) - if style == nil { - return fmt.Errorf("unknown style: %s", params.Style) - } - - formatter := html.New( - html.WithClasses(true), - html.WithCSSComments(false), - html.WithCustomCSS(map[chroma.TokenType]string{ - chroma.GenericInserted: "display: block;", - chroma.GenericDeleted: "display: block;", - }), - ) - - var cssBuf bytes.Buffer - if err := formatter.WriteCSS(&cssBuf, style); err != nil { - return err - } - - lexer := lexers.Get("diff") - if lexer == nil { - return fmt.Errorf("failed to get lexer for diff") - } - - outPath := filepath.Join(params.OutputDir, "commit", commit.Hash+".html") - - f, err := os.Create(outPath) - if err != nil { - return err - } - rootHref := filepath.ToSlash("../") - - fileTree := buildFileTree(files) - - // Create a stable order for files that matches the file tree traversal - // so that the per-file views appear in the same order as the sidebar tree. - fileOrder := make(map[string]int) - { - // Preorder traversal (dirs first, then files), respecting sortNode ordering - var idx int - var walk func(nodes []*templates.FileTree) - walk = func(nodes []*templates.FileTree) { - for _, n := range nodes { - if n.IsDir { - // Children are already sorted by sortNode - walk(n.Children) - continue - } - if n.Path == "" { - continue - } - if _, ok := fileOrder[n.Path]; !ok { - fileOrder[n.Path] = idx - idx++ - } - } - } - walk(fileTree) - } - - // Prepare per-file views - var filesViews []templates.FileView - for _, f := range files { - path := f.NewName - if f.IsDelete { - path = f.OldName - } - if path == "" { - continue - } - - var fileDiff strings.Builder - for _, frag := range f.TextFragments { - fileDiff.WriteString(frag.String()) - } - - it, err := lexer.Tokenise(nil, fileDiff.String()) - if err != nil { - return err - } - var buf bytes.Buffer - if err := formatter.Format(&buf, style, it); err != nil { - return err - } - - filesViews = append(filesViews, templates.FileView{ - Path: path, - OldName: f.OldName, - NewName: f.NewName, - IsNew: f.IsNew, - IsDelete: f.IsDelete, - IsRename: f.IsRename, - IsBinary: f.IsBinary, - HasChanges: f.TextFragments != nil, - HTML: template.HTML(buf.String()), - }) - } - - // Sort file views to match the file tree order. If for some reason a path - // is missing in the order map (shouldn't happen), fall back to case-insensitive - // alphabetical order by full path. - sort.Slice(filesViews, func(i, j int) bool { - oi, iok := fileOrder[filesViews[i].Path] - oj, jok := fileOrder[filesViews[j].Path] - if iok && jok { - return oi < oj - } - if iok != jok { - return iok // known order first - } - return filesViews[i].Path < filesViews[j].Path - }) - - currentRef := params.DefaultRef - if !commit.Branch.IsEmpty() { - currentRef = commit.Branch - } - - err = templates.CommitTemplate.ExecuteTemplate(f, "layout.gohtml", templates.CommitParams{ - LayoutParams: templates.LayoutParams{ - Title: fmt.Sprintf("%s %s %s@%s", commit.Subject, dot, params.Name, commit.ShortHash), - Name: params.Name, - Dark: params.Dark, - RootHref: rootHref, - CurrentRefDir: currentRef.DirName(), - Selected: "commits", - }, - Commit: commit, - DiffCSS: template.CSS(cssBuf.String()), - FileTree: fileTree, - FileViews: filesViews, - }) - if err != nil { - _ = f.Close() - return err - } - if err := f.Close(); err != nil { - return err - } - return nil -} - -func buildFileTree(files []*gitdiff.File) []*templates.FileTree { - // Use a synthetic root (not rendered), collect top-level nodes in a map first. - root := &templates.FileTree{IsDir: true, Name: "", Path: "", Children: nil} - - for _, f := range files { - path := f.NewName - if f.IsDelete { - path = f.OldName - } - - path = filepath.ToSlash(strings.TrimPrefix(path, "./")) - if path == "" { - continue - } - parts := strings.Split(path, "/") - - parent := root - accum := "" - if len(parts) > 1 { - for i := 0; i < len(parts)-1; i++ { - if accum == "" { - accum = parts[i] - } else { - accum = accum + "/" + parts[i] - } - parent = findOrCreateDir(parent, parts[i], accum) - } - } - - fileName := parts[len(parts)-1] - node := &templates.FileTree{ - Name: fileName, - Path: path, - IsDir: false, - IsNew: f.IsNew, - IsDelete: f.IsDelete, - IsRename: f.IsRename, - OldName: f.OldName, - NewName: f.NewName, - } - parent.Children = append(parent.Children, node) - } - - sortNode(root) - return root.Children -} - -func findOrCreateDir(parent *templates.FileTree, name, path string) *templates.FileTree { - for _, ch := range parent.Children { - if ch.IsDir && ch.Name == name { - return ch - } - } - node := &templates.FileTree{IsDir: true, Name: name, Path: path} - parent.Children = append(parent.Children, node) - return node -} - -func sortNode(n *templates.FileTree) { - if len(n.Children) == 0 { - return - } - sort.Slice(n.Children, func(i, j int) bool { - a, b := n.Children[i], n.Children[j] - if a.IsDir != b.IsDir { - return a.IsDir && !b.IsDir // dirs first - } - return strings.ToLower(a.Name) < strings.ToLower(b.Name) - }) - for _, ch := range n.Children { - if ch.IsDir { - sortNode(ch) - } - } -} diff --git a/commits_atom.go b/commits_atom.go deleted file mode 100644 index 7cfc00f..0000000 --- a/commits_atom.go +++ /dev/null @@ -1,96 +0,0 @@ -package main - -import ( - "encoding/xml" - "os" - "path/filepath" - - "mokhan.ca/antonmedv/gitmal/pkg/git" -) - -type AtomFeed struct { - XMLName xml.Name `xml:"feed"` - XMLNS string `xml:"xmlns,attr"` - ID string `xml:"id"` - Title string `xml:"title"` - Updated string `xml:"updated"` - Link []AtomLink `xml:"link"` - Entries []AtomEntry `xml:"entry"` -} - -type AtomLink struct { - Rel string `xml:"rel,attr"` - Type string `xml:"type,attr"` - Href string `xml:"href,attr"` -} - -type AtomEntry struct { - ID string `xml:"id"` - Title string `xml:"title"` - Updated string `xml:"updated"` - Author AtomAuthor `xml:"author"` - Content string `xml:"content"` - Link AtomLink `xml:"link"` -} - -type AtomAuthor struct { - Name string `xml:"name"` - Email string `xml:"email,omitempty"` -} - -func generateCommitsAtom(commits []git.Commit, params Params) error { - outDir := filepath.Join(params.OutputDir, "commits") - if err := os.MkdirAll(outDir, 0o755); err != nil { - return err - } - - var updated string - if len(commits) > 0 { - updated = commits[0].Date.Format("2006-01-02T15:04:05Z") - } - - entries := make([]AtomEntry, len(commits)) - for i, c := range commits { - content := c.Subject - if c.Body != "" { - content = c.Subject + "\n\n" + c.Body - } - entries[i] = AtomEntry{ - ID: "urn:sha:" + c.Hash, - Title: c.Subject, - Updated: c.Date.Format("2006-01-02T15:04:05Z"), - Author: AtomAuthor{Name: c.Author, Email: c.Email}, - Content: content, - Link: AtomLink{ - Rel: "alternate", - Type: "text/html", - Href: "commit/" + c.Hash + ".html", - }, - } - } - - feed := AtomFeed{ - XMLNS: "http://www.w3.org/2005/Atom", - ID: "urn:gitmal:" + params.Name + ":" + params.Ref.String(), - Title: params.Name + " commits on " + params.Ref.String(), - Updated: updated, - Link: []AtomLink{ - {Rel: "self", Type: "application/atom+xml", Href: "commits/" + params.Ref.DirName() + ".atom"}, - }, - Entries: entries, - } - - outPath := filepath.Join(outDir, params.Ref.DirName()+".atom") - f, err := os.Create(outPath) - if err != nil { - return err - } - defer f.Close() - - if _, err := f.WriteString(xml.Header); err != nil { - return err - } - encoder := xml.NewEncoder(f) - encoder.Indent("", " ") - return encoder.Encode(feed) -} diff --git a/commits_json.go b/commits_json.go deleted file mode 100644 index c9b4779..0000000 --- a/commits_json.go +++ /dev/null @@ -1,71 +0,0 @@ -package main - -import ( - "encoding/json" - "os" - "path/filepath" - - "mokhan.ca/antonmedv/gitmal/pkg/git" -) - -type CommitJSON struct { - SHA string `json:"sha"` - Commit CommitDetail `json:"commit"` - Parents []ParentRef `json:"parents"` -} - -type CommitDetail struct { - Author PersonInfo `json:"author"` - Committer PersonInfo `json:"committer"` - Message string `json:"message"` -} - -type PersonInfo struct { - Name string `json:"name"` - Email string `json:"email"` - Date string `json:"date"` -} - -type ParentRef struct { - SHA string `json:"sha"` -} - -func toCommitJSON(c git.Commit) CommitJSON { - message := c.Subject - if c.Body != "" { - message = c.Subject + "\n\n" + c.Body - } - parents := make([]ParentRef, len(c.Parents)) - for i, p := range c.Parents { - parents[i] = ParentRef{SHA: p} - } - return CommitJSON{ - SHA: c.Hash, - Commit: CommitDetail{ - Author: PersonInfo{Name: c.Author, Email: c.Email, Date: c.Date.Format("2006-01-02T15:04:05Z")}, - Committer: PersonInfo{Name: c.CommitterName, Email: c.CommitterEmail, Date: c.CommitterDate.Format("2006-01-02T15:04:05Z")}, - Message: message, - }, - Parents: parents, - } -} - -func generateCommitsJSON(commits []git.Commit, params Params) error { - outDir := filepath.Join(params.OutputDir, "commits") - if err := os.MkdirAll(outDir, 0o755); err != nil { - return err - } - list := make([]CommitJSON, len(commits)) - for i, c := range commits { - list[i] = toCommitJSON(c) - } - outPath := filepath.Join(outDir, params.Ref.DirName()+".json") - f, err := os.Create(outPath) - if err != nil { - return err - } - defer f.Close() - encoder := json.NewEncoder(f) - encoder.SetIndent("", " ") - return encoder.Encode(list) -} diff --git a/commits_list.go b/commits_list.go deleted file mode 100644 index 14b3921..0000000 --- a/commits_list.go +++ /dev/null @@ -1,101 +0,0 @@ -package main - -import ( - "fmt" - "os" - "path/filepath" - "slices" - - "mokhan.ca/antonmedv/gitmal/pkg/git" - "mokhan.ca/antonmedv/gitmal/pkg/progress_bar" - "mokhan.ca/antonmedv/gitmal/pkg/templates" -) - -const commitsPerPage = 100 - -func generateLogForBranch(allCommits []git.Commit, params Params) error { - total := len(allCommits) - totalPages := (total + commitsPerPage - 1) / commitsPerPage - - // RootHref from commits/<branch>/... => ../../ - rootHref := "../../" - outBase := filepath.Join(params.OutputDir, "commits", params.Ref.DirName()) - if err := os.MkdirAll(outBase, 0o755); err != nil { - return err - } - - p := progress_bar.NewProgressBar("commits for "+params.Ref.String(), totalPages) - - page := 1 - for pageCommits := range slices.Chunk(allCommits, commitsPerPage) { - for i := range pageCommits { - pageCommits[i].Href = filepath.ToSlash(filepath.Join(rootHref, "commit", pageCommits[i].Hash+".html")) - } - - fileName := "index.html" - if page > 1 { - fileName = fmt.Sprintf("page-%d.html", page) - } - - outPath := filepath.Join(outBase, fileName) - f, err := os.Create(outPath) - if err != nil { - return err - } - - var prevHref, nextHref, firstHref, lastHref string - if page > 1 { - if page-1 == 1 { - prevHref = "index.html" - } else { - prevHref = fmt.Sprintf("page-%d.html", page-1) - } - firstHref = "index.html" - } - - if page < totalPages { - nextHref = fmt.Sprintf("page-%d.html", page+1) - if totalPages > 1 { - lastHref = fmt.Sprintf("page-%d.html", totalPages) - } - } - - err = templates.CommitsListTemplate.ExecuteTemplate(f, "layout.gohtml", templates.CommitsListParams{ - LayoutParams: templates.LayoutParams{ - Title: fmt.Sprintf("Commits %s %s", dot, params.Name), - Name: params.Name, - Dark: params.Dark, - RootHref: rootHref, - CurrentRefDir: params.Ref.DirName(), - Selected: "commits", - }, - HeaderParams: templates.HeaderParams{ - Header: "Commits", - }, - Ref: params.Ref, - Commits: pageCommits, - Page: templates.Pagination{ - Page: page, - TotalPages: totalPages, - PrevHref: prevHref, - NextHref: nextHref, - FirstHref: firstHref, - LastHref: lastHref, - }, - }) - if err != nil { - _ = f.Close() - return err - } - if err := f.Close(); err != nil { - return err - } - - page++ - p.Inc() - } - - p.Done() - - return nil -} diff --git a/index.go b/index.go deleted file mode 100644 index 341cc71..0000000 --- a/index.go +++ /dev/null @@ -1,129 +0,0 @@ -package main - -import ( - "os" - "path/filepath" - "sort" - "strings" - - "mokhan.ca/antonmedv/gitmal/pkg/git" - "mokhan.ca/antonmedv/gitmal/pkg/links" - "mokhan.ca/antonmedv/gitmal/pkg/templates" -) - -func generateIndex(files []git.Blob, params Params) error { - // Build directory indexes - type dirInfo struct { - subdirs map[string]struct{} - files []git.Blob - } - 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 - } - - dirsSet := links.BuildDirSet(files) - filesSet := links.BuildFileSet(files) - - for _, b := range files { - // Normalize to forward slash paths for URL construction - p := b.Path - parts := strings.Split(p, "/") - // walk directories - cur := "" - for i := 0; i < len(parts)-1; i++ { - child := parts[i] - ensureDir(cur).subdirs[child] = struct{}{} - if cur == "" { - cur = child - } else { - cur = cur + "/" + child - } - ensureDir(cur) // ensure it exists - } - ensureDir(cur).files = append(ensureDir(cur).files, b) - } - - di := dirs[""] // root - - outDir := params.OutputDir - if err := os.MkdirAll(outDir, 0o755); err != nil { - return err - } - - // Build entries - dirNames := make([]string, 0, len(di.subdirs)) - for name := range di.subdirs { - dirNames = append(dirNames, name) - } - // Sort for stable output - sort.Strings(dirNames) - sort.Slice(di.files, func(i, j int) bool { - return di.files[i].FileName < di.files[j].FileName - }) - - subdirEntries := make([]templates.ListEntry, 0, len(dirNames)) - for _, name := range dirNames { - subdirEntries = append(subdirEntries, templates.ListEntry{ - Name: name + "/", - Href: "blob/" + params.Ref.DirName() + "/" + name + "/index.html", - IsDir: true, - }) - } - - fileEntries := make([]templates.ListEntry, 0, len(di.files)) - for _, b := range di.files { - fileEntries = append(fileEntries, templates.ListEntry{ - Name: b.FileName + "", - Href: "blob/" + params.Ref.DirName() + "/" + b.FileName + ".html", - Mode: b.Mode, - Size: humanizeSize(b.Size), - }) - } - - // Title and current path label - title := params.Name - - f, err := os.Create(filepath.Join(outDir, "index.html")) - if err != nil { - return err - } - - rootHref := "./" - readmeHTML := readme(di.files, dirsSet, filesSet, params, rootHref) - - err = templates.ListTemplate.ExecuteTemplate(f, "layout.gohtml", templates.ListParams{ - LayoutParams: templates.LayoutParams{ - Title: title, - Name: params.Name, - Dark: params.Dark, - CSSMarkdown: cssMarkdown(params.Dark), - RootHref: rootHref, - CurrentRefDir: params.Ref.DirName(), - Selected: "code", - }, - HeaderParams: templates.HeaderParams{ - Ref: params.Ref, - Breadcrumbs: breadcrumbs(params.Name, "", false), - }, - Ref: params.Ref, - Dirs: subdirEntries, - Files: fileEntries, - Readme: readmeHTML, - }) - if err != nil { - _ = f.Close() - return err - } - if err := f.Close(); err != nil { - return err - } - - return nil -} diff --git a/list.go b/list.go deleted file mode 100644 index dcee8f0..0000000 --- a/list.go +++ /dev/null @@ -1,249 +0,0 @@ -package main - -import ( - "context" - "fmt" - "html/template" - "os" - "path/filepath" - "runtime" - "sort" - "strings" - "sync" - - "mokhan.ca/antonmedv/gitmal/pkg/git" - "mokhan.ca/antonmedv/gitmal/pkg/links" - "mokhan.ca/antonmedv/gitmal/pkg/progress_bar" - "mokhan.ca/antonmedv/gitmal/pkg/templates" -) - -func generateLists(files []git.Blob, params Params) error { - // Build directory indexes - type dirInfo struct { - subdirs map[string]struct{} - files []git.Blob - } - 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 - } - - dirsSet := links.BuildDirSet(files) - filesSet := links.BuildFileSet(files) - - for _, b := range files { - // Normalize to forward slash paths for URL construction - p := b.Path - parts := strings.Split(p, "/") - // walk directories - cur := "" - for i := 0; i < len(parts)-1; i++ { - child := parts[i] - ensureDir(cur).subdirs[child] = struct{}{} - if cur == "" { - cur = child - } else { - cur = cur + "/" + child - } - ensureDir(cur) // ensure it exists - } - ensureDir(cur).files = append(ensureDir(cur).files, b) - } - - // Prepare jobs slice to have stable iteration order (optional) - type job struct { - dirPath string - di *dirInfo - } - jobsSlice := make([]job, 0, len(dirs)) - for dp, di := range dirs { - jobsSlice = append(jobsSlice, job{dirPath: dp, di: di}) - } - // Sort by dirPath for determinism - sort.Slice(jobsSlice, func(i, j int) bool { return jobsSlice[i].dirPath < jobsSlice[j].dirPath }) - - // Worker pool similar to generateBlobs - workers := runtime.NumCPU() - if workers < 1 { - workers = 1 - } - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - jobCh := make(chan job) - errCh := make(chan error, 1) - var wg sync.WaitGroup - - p := progress_bar.NewProgressBar("lists for "+params.Ref.String(), len(jobsSlice)) - - check := func(err error) bool { - if err != nil { - select { - case errCh <- err: - cancel() - default: - } - return true - } - return false - } - - workerFn := func() { - defer wg.Done() - for { - select { - case <-ctx.Done(): - return - case jb, ok := <-jobCh: - if !ok { - return - } - func() { - dirPath := jb.dirPath - di := jb.di - - outDir := filepath.Join(params.OutputDir, "blob", params.Ref.DirName()) - if dirPath != "" { - // convert forward slash path into OS path - outDir = filepath.Join(outDir, filepath.FromSlash(dirPath)) - } - if err := os.MkdirAll(outDir, 0o755); check(err) { - return - } - - // Build entries - dirNames := make([]string, 0, len(di.subdirs)) - for name := range di.subdirs { - dirNames = append(dirNames, name) - } - - // Sort for stable output - sort.Strings(dirNames) - sort.Slice(di.files, func(i, j int) bool { - return di.files[i].FileName < di.files[j].FileName - }) - - subdirEntries := make([]templates.ListEntry, 0, len(dirNames)) - for _, name := range dirNames { - subdirEntries = append(subdirEntries, templates.ListEntry{ - Name: name + "/", - Href: name + "/index.html", - IsDir: true, - }) - } - - fileEntries := make([]templates.ListEntry, 0, len(di.files)) - for _, b := range di.files { - fileEntries = append(fileEntries, templates.ListEntry{ - Name: b.FileName + "", - Href: b.FileName + ".html", - Mode: b.Mode, - Size: humanizeSize(b.Size), - }) - } - - // Title and current path label - title := fmt.Sprintf("%s/%s at %s", params.Name, dirPath, params.Ref) - if dirPath == "" { - title = fmt.Sprintf("%s at %s", params.Name, params.Ref) - } - - f, err := os.Create(filepath.Join(outDir, "index.html")) - if check(err) { - return - } - defer func() { - _ = f.Close() - }() - - // parent link is not shown for root - parent := "../index.html" - if dirPath == "" { - parent = "" - } - - depth := 0 - if dirPath != "" { - depth = len(strings.Split(dirPath, "/")) - } - rootHref := strings.Repeat("../", depth+2) - - readmeHTML := readme(di.files, dirsSet, filesSet, params, rootHref) - var CSSMarkdown template.CSS - if readmeHTML != "" { - CSSMarkdown = cssMarkdown(params.Dark) - } - - err = templates.ListTemplate.ExecuteTemplate(f, "layout.gohtml", templates.ListParams{ - LayoutParams: templates.LayoutParams{ - Title: title, - Name: params.Name, - Dark: params.Dark, - CSSMarkdown: CSSMarkdown, - RootHref: rootHref, - CurrentRefDir: params.Ref.DirName(), - Selected: "code", - }, - HeaderParams: templates.HeaderParams{ - Ref: params.Ref, - Breadcrumbs: breadcrumbs(params.Name, dirPath, false), - }, - Ref: params.Ref, - ParentHref: parent, - Dirs: subdirEntries, - Files: fileEntries, - Readme: readmeHTML, - }) - if check(err) { - return - } - }() - - p.Inc() - } - } - } - - // Start workers - wg.Add(workers) - for i := 0; i < workers; i++ { - go workerFn() - } - - // Feed jobs - go func() { - defer close(jobCh) - for _, jb := range jobsSlice { - select { - case <-ctx.Done(): - return - case jobCh <- jb: - } - } - }() - - // Wait for workers or first error - doneCh := make(chan struct{}) - go func() { - wg.Wait() - close(doneCh) - }() - - var runErr error - select { - case runErr = <-errCh: - <-doneCh - case <-doneCh: - } - - p.Done() - - return runErr -} diff --git a/main.go b/main.go deleted file mode 100644 index 5568e73..0000000 --- a/main.go +++ /dev/null @@ -1,289 +0,0 @@ -package main - -import ( - "fmt" - "os" - "path/filepath" - "regexp" - "runtime/pprof" - "strings" - - "mokhan.ca/antonmedv/gitmal/pkg/git" - - flag "github.com/spf13/pflag" -) - -var ( - flagOwner string - flagName string - flagOutput string - flagBranches string - flagDefaultBranch string - flagTheme string - flagPreviewThemes bool - flagMinify bool - flagGzip bool -) - -type Params struct { - Owner string - Name string - RepoDir string - Ref git.Ref - OutputDir string - Style string - Dark bool - DefaultRef git.Ref -} - -func main() { - if _, ok := os.LookupEnv("GITMAL_PPROF"); ok { - f, err := os.Create("cpu.prof") - if err != nil { - panic(err) - } - err = pprof.StartCPUProfile(f) - if err != nil { - panic(err) - } - defer f.Close() - defer pprof.StopCPUProfile() - memProf, err := os.Create("mem.prof") - if err != nil { - panic(err) - } - defer memProf.Close() - defer pprof.WriteHeapProfile(memProf) - } - - _, noFiles := os.LookupEnv("NO_FILES") - _, noCommitsList := os.LookupEnv("NO_COMMITS_LIST") - - flag.StringVar(&flagOwner, "owner", "", "Project owner") - flag.StringVar(&flagName, "name", "", "Project name") - flag.StringVar(&flagOutput, "output", "output", "Output directory for generated HTML files") - flag.StringVar(&flagBranches, "branches", "", "Regex for branches to include") - flag.StringVar(&flagDefaultBranch, "default-branch", "", "Default branch to use (autodetect master or main)") - flag.StringVar(&flagTheme, "theme", "github", "Style theme") - flag.BoolVar(&flagPreviewThemes, "preview-themes", false, "Preview available themes") - flag.BoolVar(&flagMinify, "minify", false, "Minify all generated HTML files") - flag.BoolVar(&flagGzip, "gzip", false, "Compress all generated HTML files") - flag.Usage = usage - flag.Parse() - - input := "." - args := flag.Args() - if len(args) == 1 { - input = args[0] - } - if len(args) > 1 { - panic("Multiple repos not supported yet") - } - - if flagPreviewThemes { - previewThemes() - os.Exit(0) - } - - outputDir, err := filepath.Abs(flagOutput) - if err != nil { - panic(err) - } - - absInput, err := filepath.Abs(input) - if err != nil { - panic(err) - } - input = absInput - - if flagName == "" { - flagName = filepath.Base(input) - flagName = strings.TrimSuffix(flagName, ".git") - } - - themeColor, ok := themeStyles[flagTheme] - if !ok { - panic("Invalid theme: " + flagTheme) - } - - branchesFilter, err := regexp.Compile(flagBranches) - if err != nil { - panic(err) - } - - branches, err := git.Branches(input, branchesFilter, flagDefaultBranch) - if err != nil { - panic(err) - } - - tags, err := git.Tags(input) - if err != nil { - panic(err) - } - - if flagDefaultBranch == "" { - if containsBranch(branches, "master") { - flagDefaultBranch = "master" - } else if containsBranch(branches, "main") { - flagDefaultBranch = "main" - } else { - echo("No default branch found. Specify one using --default-branch flag.") - os.Exit(1) - } - } - - if !containsBranch(branches, flagDefaultBranch) { - echo(fmt.Sprintf("Default branch %q not found.", flagDefaultBranch)) - echo("Specify a valid branch using --default-branch flag.") - os.Exit(1) - } - - if yes, a, b := hasConflictingBranchNames(branches); yes { - echo(fmt.Sprintf("Conflicting branchs %q and %q, both want to use %q dir name.", a, b, a.DirName())) - os.Exit(1) - } - - // Start generating pages - - params := Params{ - Owner: flagOwner, - Name: flagName, - RepoDir: input, - OutputDir: outputDir, - Style: flagTheme, - Dark: themeColor == "dark", - DefaultRef: git.NewRef(flagDefaultBranch), - } - - commits := make(map[string]git.Commit) - commitsFor := make(map[git.Ref][]git.Commit, len(branches)) - - for _, branch := range branches { - commitsFor[branch], err = git.Commits(branch, params.RepoDir) - if err != nil { - panic(err) - } - - for _, commit := range commitsFor[branch] { - if alreadyExisting, ok := commits[commit.Hash]; ok && alreadyExisting.Branch == params.DefaultRef { - continue - } - commit.Branch = branch - commits[commit.Hash] = commit - } - } - - // Add commits from tags - for _, tag := range tags { - commitsForTag, err := git.Commits(git.NewRef(tag.Name), params.RepoDir) - if err != nil { - panic(err) - } - for _, commit := range commitsForTag { - // Only add new commits - if alreadyExisting, ok := commits[commit.Hash]; ok && !alreadyExisting.Branch.IsEmpty() { - continue - } - commits[commit.Hash] = commit - } - } - - echo(fmt.Sprintf("> %s: %d branches, %d tags, %d commits", params.Name, len(branches), len(tags), len(commits))) - - if err := generateBranches(branches, flagDefaultBranch, params); err != nil { - panic(err) - } - - if err := generateBranchesJSON(branches, commitsFor, params); err != nil { - panic(err) - } - - var defaultBranchFiles []git.Blob - - for i, branch := range branches { - echo(fmt.Sprintf("> [%d/%d] %s@%s", i+1, len(branches), params.Name, branch)) - params.Ref = branch - - if !noFiles { - files, err := git.Files(params.Ref, params.RepoDir) - if err != nil { - panic(err) - } - - if branch.String() == flagDefaultBranch { - defaultBranchFiles = files - } - - err = generateBlobs(files, params) - if err != nil { - panic(err) - } - - err = generateLists(files, params) - if err != nil { - panic(err) - } - } - - if !noCommitsList { - err = generateLogForBranch(commitsFor[branch], params) - if err != nil { - panic(err) - } - - if err := generateCommitsJSON(commitsFor[branch], params); err != nil { - panic(err) - } - - if err := generateCommitsAtom(commitsFor[branch], params); err != nil { - panic(err) - } - } - } - - // Back to the default branch - params.Ref = git.NewRef(flagDefaultBranch) - - // Commits pages generation - echo("> generating commits...") - err = generateCommits(commits, params) - if err != nil { - panic(err) - } - - // Tags page generation - if err := generateTags(tags, params); err != nil { - panic(err) - } - - if err := generateTagsAtom(tags, params); err != nil { - panic(err) - } - - if err := generateReleasesAtom(tags, params); err != nil { - panic(err) - } - - // Index page generation - if !noFiles { - if len(defaultBranchFiles) == 0 { - panic("No files found for default branch") - } - err = generateIndex(defaultBranchFiles, params) - if err != nil { - panic(err) - } - } - - if flagMinify || flagGzip { - echo("> post-processing HTML...") - if err := postProcessHTML(params.OutputDir, flagMinify, flagGzip); err != nil { - panic(err) - } - } -} - -func usage() { - fmt.Fprintf(os.Stderr, "Usage: gitmal [options] [path ...]\n") - flag.PrintDefaults() -} diff --git a/markdown.go b/markdown.go deleted file mode 100644 index a2a48b5..0000000 --- a/markdown.go +++ /dev/null @@ -1,38 +0,0 @@ -package main - -import ( - "html/template" - - "github.com/yuin/goldmark" - highlighting "github.com/yuin/goldmark-highlighting/v2" - "github.com/yuin/goldmark/extension" - "github.com/yuin/goldmark/parser" - gmhtml "github.com/yuin/goldmark/renderer/html" - - "mokhan.ca/antonmedv/gitmal/pkg/templates" -) - -func createMarkdown(style string) goldmark.Markdown { - return goldmark.New( - goldmark.WithExtensions( - extension.GFM, - extension.Typographer, - highlighting.NewHighlighting( - highlighting.WithStyle(style), - ), - ), - goldmark.WithParserOptions( - parser.WithAutoHeadingID(), - ), - goldmark.WithRendererOptions( - gmhtml.WithUnsafe(), - ), - ) -} - -func cssMarkdown(dark bool) template.CSS { - if dark { - return template.CSS(templates.CSSMarkdownDark) - } - return template.CSS(templates.CSSMarkdownLight) -} diff --git a/post_process.go b/post_process.go deleted file mode 100644 index 895d933..0000000 --- a/post_process.go +++ /dev/null @@ -1,139 +0,0 @@ -package main - -import ( - "bytes" - "compress/gzip" - "io" - "io/fs" - "os" - "path/filepath" - "runtime" - "strings" - "sync" - - "github.com/tdewolff/minify/v2" - "github.com/tdewolff/minify/v2/css" - "github.com/tdewolff/minify/v2/html" - "github.com/tdewolff/minify/v2/svg" - - "mokhan.ca/antonmedv/gitmal/pkg/progress_bar" -) - -func postProcessHTML(root string, doMinify bool, doGzip bool) error { - // 1) Collect all HTML files first - var files []string - if err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - if d.IsDir() { - return nil - } - if strings.HasSuffix(d.Name(), ".html") { - files = append(files, path) - } - return nil - }); err != nil { - return err - } - - if len(files) == 0 { - return nil - } - - // 2) Setup progress bar - labels := []string{} - if doMinify { - labels = append(labels, "minify") - } - if doGzip { - labels = append(labels, "gzip") - } - pb := progress_bar.NewProgressBar(strings.Join(labels, " + "), len(files)) - defer pb.Done() - - // 3) Worker pool - workers := runtime.NumCPU() - if workers < 1 { - workers = 1 - } - jobs := make(chan string, workers*2) - var wg sync.WaitGroup - var mu sync.Mutex - var firstErr error - - workerFn := func() { - defer wg.Done() - var m *minify.M - if doMinify { - m = minify.New() - m.AddFunc("text/html", html.Minify) - m.AddFunc("text/css", css.Minify) - m.AddFunc("image/svg+xml", svg.Minify) - } - for path := range jobs { - data, err := os.ReadFile(path) - if err == nil && doMinify { - if md, e := m.Bytes("text/html", data); e == nil { - data = md - } else { - err = e - } - } - if err == nil { - if doGzip { - // write to file.html.gz - gzPath := path + ".gz" - if e := writeGzip(gzPath, data); e != nil { - err = e - } else if e := os.Remove(path); e != nil { // remove original .html - err = e - } - } else { - if e := os.WriteFile(path, data, 0o644); e != nil { - err = e - } - } - } - - if err != nil { - mu.Lock() - if firstErr == nil { - firstErr = err - } - mu.Unlock() - } - pb.Inc() - } - } - - wg.Add(workers) - for i := 0; i < workers; i++ { - go workerFn() - } - for _, f := range files { - jobs <- f - } - close(jobs) - wg.Wait() - - return firstErr -} - -func writeGzip(path string, data []byte) error { - f, err := os.Create(path) - if err != nil { - return err - } - defer func() { _ = f.Close() }() - gw := gzip.NewWriter(f) - gw.Name = filepath.Base(strings.TrimSuffix(path, ".gz")) - if _, err := io.Copy(gw, bytes.NewReader(data)); err != nil { - _ = gw.Close() - return err - } - if err := gw.Close(); err != nil { - return err - } - return nil -} diff --git a/readme.go b/readme.go deleted file mode 100644 index ecdb500..0000000 --- a/readme.go +++ /dev/null @@ -1,45 +0,0 @@ -package main - -import ( - "bytes" - "html/template" - "strings" - - "mokhan.ca/antonmedv/gitmal/pkg/git" - "mokhan.ca/antonmedv/gitmal/pkg/links" -) - -func readme(files []git.Blob, dirsSet, filesSet links.Set, params Params, rootHref string) template.HTML { - var readmeHTML template.HTML - - md := createMarkdown(params.Style) - - for _, b := range files { - nameLower := strings.ToLower(b.FileName) - if strings.HasPrefix(nameLower, "readme") && isMarkdown(b.Path) { - data, isBin, err := git.BlobContent(params.Ref, b.Path, params.RepoDir) - if err != nil || isBin { - break - } - var buf bytes.Buffer - if err := md.Convert(data, &buf); err != nil { - break - } - - // Fix links/images relative to README location - htmlStr := links.Resolve( - buf.String(), - b.Path, - rootHref, - params.Ref.DirName(), - dirsSet, - filesSet, - ) - - readmeHTML = template.HTML(htmlStr) - break - } - } - - return readmeHTML -} diff --git a/tags.go b/tags.go deleted file mode 100644 index fc8ebba..0000000 --- a/tags.go +++ /dev/null @@ -1,37 +0,0 @@ -package main - -import ( - "fmt" - "os" - "path/filepath" - - "mokhan.ca/antonmedv/gitmal/pkg/git" - "mokhan.ca/antonmedv/gitmal/pkg/templates" -) - -func generateTags(entries []git.Tag, params Params) error { - outDir := params.OutputDir - if err := os.MkdirAll(outDir, 0o755); err != nil { - return err - } - - f, err := os.Create(filepath.Join(outDir, "tags.html")) - if err != nil { - return err - } - defer f.Close() - - rootHref := "./" - - return templates.TagsTemplate.ExecuteTemplate(f, "layout.gohtml", templates.TagsParams{ - LayoutParams: templates.LayoutParams{ - Title: fmt.Sprintf("Tags %s %s", dot, params.Name), - Name: params.Name, - Dark: params.Dark, - RootHref: rootHref, - CurrentRefDir: params.DefaultRef.DirName(), - Selected: "tags", - }, - Tags: entries, - }) -} diff --git a/tags_atom.go b/tags_atom.go deleted file mode 100644 index 9b1233e..0000000 --- a/tags_atom.go +++ /dev/null @@ -1,111 +0,0 @@ -package main - -import ( - "encoding/xml" - "os" - "path/filepath" - - "mokhan.ca/antonmedv/gitmal/pkg/git" -) - -func generateTagsAtom(tags []git.Tag, params Params) error { - if len(tags) == 0 { - return nil - } - - var updated string - if len(tags) > 0 { - updated = tags[0].Date.Format("2006-01-02T15:04:05Z") - } - - entries := make([]AtomEntry, len(tags)) - for i, t := range tags { - entries[i] = AtomEntry{ - ID: "urn:tag:" + t.Name, - Title: t.Name, - Updated: t.Date.Format("2006-01-02T15:04:05Z"), - Content: "Tag " + t.Name + " pointing to " + t.CommitHash[:7], - Link: AtomLink{ - Rel: "alternate", - Type: "text/html", - Href: "commit/" + t.CommitHash + ".html", - }, - } - } - - feed := AtomFeed{ - XMLNS: "http://www.w3.org/2005/Atom", - ID: "urn:gitmal:" + params.Name + ":tags", - Title: params.Name + " tags", - Updated: updated, - Link: []AtomLink{ - {Rel: "self", Type: "application/atom+xml", Href: "tags.atom"}, - }, - Entries: entries, - } - - outPath := filepath.Join(params.OutputDir, "tags.atom") - f, err := os.Create(outPath) - if err != nil { - return err - } - defer f.Close() - - if _, err := f.WriteString(xml.Header); err != nil { - return err - } - encoder := xml.NewEncoder(f) - encoder.Indent("", " ") - return encoder.Encode(feed) -} - -func generateReleasesAtom(tags []git.Tag, params Params) error { - if len(tags) == 0 { - return nil - } - - var updated string - if len(tags) > 0 { - updated = tags[0].Date.Format("2006-01-02T15:04:05Z") - } - - entries := make([]AtomEntry, len(tags)) - for i, t := range tags { - entries[i] = AtomEntry{ - ID: "urn:release:" + t.Name, - Title: t.Name, - Updated: t.Date.Format("2006-01-02T15:04:05Z"), - Content: "Release " + t.Name, - Link: AtomLink{ - Rel: "alternate", - Type: "text/html", - Href: "commit/" + t.CommitHash + ".html", - }, - } - } - - feed := AtomFeed{ - XMLNS: "http://www.w3.org/2005/Atom", - ID: "urn:gitmal:" + params.Name + ":releases", - Title: params.Name + " releases", - Updated: updated, - Link: []AtomLink{ - {Rel: "self", Type: "application/atom+xml", Href: "releases.atom"}, - }, - Entries: entries, - } - - outPath := filepath.Join(params.OutputDir, "releases.atom") - f, err := os.Create(outPath) - if err != nil { - return err - } - defer f.Close() - - if _, err := f.WriteString(xml.Header); err != nil { - return err - } - encoder := xml.NewEncoder(f) - encoder.Indent("", " ") - return encoder.Encode(feed) -} diff --git a/themes.go b/themes.go deleted file mode 100644 index ef9dc1c..0000000 --- a/themes.go +++ /dev/null @@ -1,156 +0,0 @@ -package main - -import ( - "html/template" - "net" - "net/http" - "sort" - "strings" - - "github.com/alecthomas/chroma/v2/formatters/html" - "github.com/alecthomas/chroma/v2/lexers" - "github.com/alecthomas/chroma/v2/styles" - - "mokhan.ca/antonmedv/gitmal/pkg/templates" -) - -var themeStyles = map[string]string{ - "abap": "light", - "algol": "light", - "arduino": "light", - "autumn": "light", - "average": "dark", - "base16-snazzy": "dark", - "borland": "light", - "bw": "light", - "catppuccin-frappe": "dark", - "catppuccin-latte": "light", - "catppuccin-macchiato": "dark", - "catppuccin-mocha": "dark", - "colorful": "light", - "doom-one": "dark", - "doom-one2": "dark", - "dracula": "dark", - "emacs": "light", - "evergarden": "dark", - "friendly": "light", - "fruity": "dark", - "github-dark": "dark", - "github": "light", - "gruvbox-light": "light", - "gruvbox": "dark", - "hrdark": "dark", - "igor": "light", - "lovelace": "light", - "manni": "light", - "modus-operandi": "light", - "modus-vivendi": "dark", - "monokai": "dark", - "monokailight": "light", - "murphy": "light", - "native": "dark", - "nord": "dark", - "nordic": "dark", - "onedark": "dark", - "onesenterprise": "dark", - "paraiso-dark": "dark", - "paraiso-light": "light", - "pastie": "light", - "perldoc": "light", - "pygments": "light", - "rainbow_dash": "light", - "rose-pine-dawn": "light", - "rose-pine-moon": "dark", - "rose-pine": "dark", - "rpgle": "dark", - "rrt": "dark", - "solarized-dark": "dark", - "solarized-dark256": "dark", - "solarized-light": "light", - "swapoff": "dark", - "tango": "light", - "tokyonight-day": "light", - "tokyonight-moon": "dark", - "tokyonight-night": "dark", - "tokyonight-storm": "dark", - "trac": "light", - "vim": "dark", - "vs": "light", - "vulcan": "dark", - "witchhazel": "dark", - "xcode-dark": "dark", - "xcode": "light", -} - -func previewThemes() { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - names := make([]string, 0, len(themeStyles)) - for name := range themeStyles { - names = append(names, name) - } - sort.Strings(names) - - sampleLang := "javascript" - sampleCode := `function fib(n) { - if (n <= 1) { - return n; - } - return fib(n - 1) + fib(n - 2); -} - -// Print n Fibonacci numbers. -const n = 10; - -for (let i = 0; i < n; i++) { - console.log(fib(i)); -}` - - formatter := html.New( - html.WithClasses(false), - ) - - // Generate cards - cards := make([]templates.PreviewCard, 0, len(names)) - for _, theme := range names { - style := styles.Get(theme) - if style == nil { - continue - } - lexer := lexers.Get(sampleLang) - if lexer == nil { - continue - } - it, err := lexer.Tokenise(nil, sampleCode) - if err != nil { - continue - } - var sb strings.Builder - if err := formatter.Format(&sb, style, it); err != nil { - continue - } - cards = append(cards, templates.PreviewCard{ - Name: theme, - Tone: themeStyles[theme], - HTML: template.HTML(sb.String()), - }) - } - - w.Header().Set("Content-Type", "text/html; charset=utf-8") - _ = templates.PreviewTemplate.Execute(w, templates.PreviewParams{ - Count: len(cards), - Themes: cards, - }) - }) - - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - panic(err) - } - - addr := ln.Addr().String() - echo("Preview themes at http://" + addr) - - if err := http.Serve(ln, handler); err != nil && err != http.ErrServerClosed { - panic(err) - } -} diff --git a/utils.go b/utils.go deleted file mode 100644 index 0b096c6..0000000 --- a/utils.go +++ /dev/null @@ -1,126 +0,0 @@ -package main - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "mokhan.ca/antonmedv/gitmal/pkg/git" - "mokhan.ca/antonmedv/gitmal/pkg/templates" -) - -const dot = "ยท" - -func echo(a ...any) { - _, _ = fmt.Fprintln(os.Stderr, a...) -} - -func breadcrumbs(rootName string, path string, isFile bool) []templates.Breadcrumb { - // Root list - if path == "" { - return []templates.Breadcrumb{ - { - Name: rootName, - Href: "./index.html", - IsDir: true, - }, - } - } - - // Paths from git are already with '/' - parts := strings.Split(path, "/") - - // Build breadcrumbs relative to the file location so links work in static output - // Example: for a/b/c.txt, at /blob/<ref>/a/b/c.txt.html - // - root: ../../index.html - // - a: ../index.html - // - b: index.html - // - c.txt: (no link) - d := len(parts) - - // current directory depth relative to ref - if isFile { - d -= 1 - } - - crumbs := make([]templates.Breadcrumb, 0, len(parts)) - - // root - crumbs = append(crumbs, templates.Breadcrumb{ - Name: rootName, - Href: "./" + strings.Repeat("../", d) + "index.html", - IsDir: true, - }) - - // intermediate directories - for i := 0; i < len(parts)-1; i++ { - name := parts[i] - // target directory depth t = i+1 - up := d - (i + 1) - href := "./" + strings.Repeat("../", up) + "index.html" - crumbs = append(crumbs, templates.Breadcrumb{ - Name: name, - Href: href, - IsDir: true, - }) - } - - // final file (no link) - crumbs = append(crumbs, templates.Breadcrumb{ - Name: parts[len(parts)-1], - IsDir: !isFile, - }) - - return crumbs -} - -func humanizeSize(size int64) string { - const unit = 1024 - if size < unit { - return fmt.Sprintf("%d B", size) - } - div, exp := int64(unit), 0 - for n := size / unit; n >= unit; n /= unit { - div *= unit - exp++ - } - return fmt.Sprintf("%.1f %cB", float64(size)/float64(div), "KMGTPE"[exp]) -} - -func isMarkdown(path string) bool { - lower := strings.ToLower(path) - if strings.HasSuffix(lower, ".md") || strings.HasSuffix(lower, ".markdown") || strings.HasSuffix(lower, ".mdown") || strings.HasSuffix(lower, ".mkd") || strings.HasSuffix(lower, ".mkdown") { - return true - } - return false -} - -func isImage(path string) bool { - switch filepath.Ext(path) { - case ".png", ".jpg", ".jpeg", ".gif", ".webp": - return true - default: - return false - } -} - -func containsBranch(branches []git.Ref, branch string) bool { - for _, b := range branches { - if b.String() == branch { - return true - } - } - return false -} - -func hasConflictingBranchNames(branches []git.Ref) (bool, git.Ref, git.Ref) { - uniq := make(map[string]git.Ref, len(branches)) - for _, b := range branches { - if a, exists := uniq[b.DirName()]; exists { - return true, a, b - } - uniq[b.DirName()] = b - } - return false, git.Ref{}, git.Ref{} -} |
