package generator
import (
"bytes"
"fmt"
"html/template"
"os"
"path/filepath"
"sort"
"strings"
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
"github.com/bluekeyes/go-gitdiff/gitdiff"
"mokhan.ca/xlgmokha/gitmal/internal/git"
"mokhan.ca/xlgmokha/gitmal/internal/templates"
)
func GenerateComparePages(tags []git.Tag, branches []git.Ref, params Params) error {
outDir := filepath.Join(params.OutputDir, "compare")
if err := os.MkdirAll(outDir, 0o755); err != nil {
return err
}
tags = filterValidTags(tags, params.RepoDir)
if len(tags) > 0 {
latestTag := tags[0].Name
head := params.DefaultRef.String()
if err := generateComparePage(latestTag, head, params); err != nil {
Echo(fmt.Sprintf(" warning: compare %s...%s failed: %v", latestTag, head, err))
}
}
if len(tags) > 1 {
for i := 0; i < len(tags)-1; i++ {
base := tags[i+1].Name
head := tags[i].Name
if err := generateComparePage(base, head, params); err != nil {
Echo(fmt.Sprintf(" warning: compare %s...%s failed: %v", base, head, err))
}
}
}
defaultBranch := params.DefaultRef.String()
for _, branch := range branches {
if branch.String() == defaultBranch {
continue
}
if err := generateComparePage(defaultBranch, branch.String(), params); err != nil {
Echo(fmt.Sprintf(" warning: compare %s...%s failed: %v", defaultBranch, branch, err))
}
}
return nil
}
func generateComparePage(base, head string, params Params) error {
baseRef := git.NewRef(base)
headRef := git.NewRef(head)
diff, err := git.CompareDiff(base, head, params.RepoDir)
if err != nil {
return err
}
commits, err := git.CompareCommits(baseRef, headRef, params.RepoDir)
if err != nil {
return err
}
files, _, err := gitdiff.Parse(strings.NewReader(diff))
if err != nil {
return err
}
formatter := html.New(
html.WithClasses(true),
html.WithCSSComments(false),
)
lightStyle := styles.Get("github")
lexer := lexers.Get("diff")
if lexer == nil {
return fmt.Errorf("failed to get lexer for diff")
}
fileTree := buildFileTree(files)
fileOrder := make(map[string]int)
{
var idx int
var walk func(nodes []*templates.FileTree)
walk = func(nodes []*templates.FileTree) {
for _, n := range nodes {
if n.IsDir {
walk(n.Children)
continue
}
if n.Path == "" {
continue
}
if _, ok := fileOrder[n.Path]; !ok {
fileOrder[n.Path] = idx
idx++
}
}
}
walk(fileTree)
}
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, lightStyle, 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.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
}
return filesViews[i].Path < filesViews[j].Path
})
for i := range commits {
commits[i].Href = filepath.ToSlash(filepath.Join("../../commit", commits[i].Hash+".html"))
}
headDirName := headRef.DirName()
if head == "HEAD" {
headDirName = "HEAD"
}
dirName := baseRef.DirName() + "..." + headDirName
outDir := filepath.Join(params.OutputDir, "compare", dirName)
if err := os.MkdirAll(outDir, 0o755); err != nil {
return err
}
outPath := filepath.Join(outDir, "index.html")
f, err := os.Create(outPath)
if err != nil {
return err
}
rootHref := "../../"
err = templates.CompareTemplate.ExecuteTemplate(f, "layout.gohtml", templates.CompareParams{
LayoutParams: templates.LayoutParams{
Title: fmt.Sprintf("Comparing %s...%s %s %s", base, head, Dot, params.Name),
Name: params.Name,
RootHref: rootHref,
CurrentRefDir: params.DefaultRef.DirName(),
Selected: "",
NeedsSyntaxCSS: true,
},
Base: base,
Head: head,
Commits: commits,
FileTree: fileTree,
FileViews: filesViews,
})
if err != nil {
_ = f.Close()
return err
}
return f.Close()
}
func filterValidTags(tags []git.Tag, repoDir string) []git.Tag {
valid := make([]git.Tag, 0, len(tags))
for _, tag := range tags {
if git.RefExists(tag.Name, repoDir) {
valid = append(valid, tag)
}
}
return valid
}