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 /commit.go | |
| parent | ff213f4113434fee4ddba1f0874d3c2c99b4f9de (diff) | |
refactor: move main.go to cmd/gitmal/
Diffstat (limited to 'commit.go')
| -rw-r--r-- | commit.go | 335 |
1 files changed, 0 insertions, 335 deletions
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) - } - } -} |
