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}