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 }