main
1package generator
2
3import (
4 "bytes"
5 "fmt"
6 "html/template"
7 "os"
8 "path/filepath"
9 "sort"
10 "strings"
11
12 "github.com/alecthomas/chroma/v2/formatters/html"
13 "github.com/alecthomas/chroma/v2/lexers"
14 "github.com/alecthomas/chroma/v2/styles"
15 "github.com/bluekeyes/go-gitdiff/gitdiff"
16
17 "mokhan.ca/xlgmokha/gitmal/internal/git"
18 "mokhan.ca/xlgmokha/gitmal/internal/templates"
19)
20
21func GenerateComparePages(tags []git.Tag, branches []git.Ref, params Params) error {
22 outDir := filepath.Join(params.OutputDir, "compare")
23 if err := os.MkdirAll(outDir, 0o755); err != nil {
24 return err
25 }
26
27 tags = filterValidTags(tags, params.RepoDir)
28
29 if len(tags) > 0 {
30 latestTag := tags[0].Name
31 head := params.DefaultRef.String()
32 if err := generateComparePage(latestTag, head, params); err != nil {
33 Echo(fmt.Sprintf(" warning: compare %s...%s failed: %v", latestTag, head, err))
34 }
35 }
36
37 if len(tags) > 1 {
38 for i := 0; i < len(tags)-1; i++ {
39 base := tags[i+1].Name
40 head := tags[i].Name
41 if err := generateComparePage(base, head, params); err != nil {
42 Echo(fmt.Sprintf(" warning: compare %s...%s failed: %v", base, head, err))
43 }
44 }
45 }
46
47 defaultBranch := params.DefaultRef.String()
48 for _, branch := range branches {
49 if branch.String() == defaultBranch {
50 continue
51 }
52 if err := generateComparePage(defaultBranch, branch.String(), params); err != nil {
53 Echo(fmt.Sprintf(" warning: compare %s...%s failed: %v", defaultBranch, branch, err))
54 }
55 }
56
57 return nil
58}
59
60func generateComparePage(base, head string, params Params) error {
61 baseRef := git.NewRef(base)
62 headRef := git.NewRef(head)
63
64 diff, err := git.CompareDiff(base, head, params.RepoDir)
65 if err != nil {
66 return err
67 }
68
69 commits, err := git.CompareCommits(baseRef, headRef, params.RepoDir)
70 if err != nil {
71 return err
72 }
73
74 files, _, err := gitdiff.Parse(strings.NewReader(diff))
75 if err != nil {
76 return err
77 }
78
79 formatter := html.New(
80 html.WithClasses(true),
81 html.WithCSSComments(false),
82 )
83 lightStyle := styles.Get("github")
84 lexer := lexers.Get("diff")
85 if lexer == nil {
86 return fmt.Errorf("failed to get lexer for diff")
87 }
88
89 fileTree := buildFileTree(files)
90 fileOrder := make(map[string]int)
91 {
92 var idx int
93 var walk func(nodes []*templates.FileTree)
94 walk = func(nodes []*templates.FileTree) {
95 for _, n := range nodes {
96 if n.IsDir {
97 walk(n.Children)
98 continue
99 }
100 if n.Path == "" {
101 continue
102 }
103 if _, ok := fileOrder[n.Path]; !ok {
104 fileOrder[n.Path] = idx
105 idx++
106 }
107 }
108 }
109 walk(fileTree)
110 }
111
112 var filesViews []templates.FileView
113 for _, f := range files {
114 path := f.NewName
115 if f.IsDelete {
116 path = f.OldName
117 }
118 if path == "" {
119 continue
120 }
121
122 var fileDiff strings.Builder
123 for _, frag := range f.TextFragments {
124 fileDiff.WriteString(frag.String())
125 }
126
127 it, err := lexer.Tokenise(nil, fileDiff.String())
128 if err != nil {
129 return err
130 }
131 var buf bytes.Buffer
132 if err := formatter.Format(&buf, lightStyle, it); err != nil {
133 return err
134 }
135
136 filesViews = append(filesViews, templates.FileView{
137 Path: path,
138 OldName: f.OldName,
139 NewName: f.NewName,
140 IsNew: f.IsNew,
141 IsDelete: f.IsDelete,
142 IsRename: f.IsRename,
143 IsBinary: f.IsBinary,
144 HasChanges: f.TextFragments != nil,
145 HTML: template.HTML(buf.String()),
146 })
147 }
148
149 sort.Slice(filesViews, func(i, j int) bool {
150 oi, iok := fileOrder[filesViews[i].Path]
151 oj, jok := fileOrder[filesViews[j].Path]
152 if iok && jok {
153 return oi < oj
154 }
155 if iok != jok {
156 return iok
157 }
158 return filesViews[i].Path < filesViews[j].Path
159 })
160
161 for i := range commits {
162 commits[i].Href = filepath.ToSlash(filepath.Join("../../commit", commits[i].Hash+".html"))
163 }
164
165 headDirName := headRef.DirName()
166 if head == "HEAD" {
167 headDirName = "HEAD"
168 }
169 dirName := baseRef.DirName() + "..." + headDirName
170 outDir := filepath.Join(params.OutputDir, "compare", dirName)
171 if err := os.MkdirAll(outDir, 0o755); err != nil {
172 return err
173 }
174 outPath := filepath.Join(outDir, "index.html")
175 f, err := os.Create(outPath)
176 if err != nil {
177 return err
178 }
179
180 rootHref := "../../"
181
182 err = templates.CompareTemplate.ExecuteTemplate(f, "layout.gohtml", templates.CompareParams{
183 LayoutParams: templates.LayoutParams{
184 Title: fmt.Sprintf("Comparing %s...%s %s %s", base, head, Dot, params.Name),
185 Name: params.Name,
186 RootHref: rootHref,
187 CurrentRefDir: params.DefaultRef.DirName(),
188 Selected: "",
189 NeedsSyntaxCSS: true,
190 Year: currentYear(),
191 },
192 Base: base,
193 Head: head,
194 Commits: commits,
195 FileTree: fileTree,
196 FileViews: filesViews,
197 })
198 if err != nil {
199 _ = f.Close()
200 return err
201 }
202 return f.Close()
203}
204
205func filterValidTags(tags []git.Tag, repoDir string) []git.Tag {
206 valid := make([]git.Tag, 0, len(tags))
207 for _, tag := range tags {
208 if git.RefExists(tag.Name, repoDir) {
209 valid = append(valid, tag)
210 }
211 }
212 return valid
213}