// SPDX-FileCopyrightText: 2025 Romain Maneschi // // SPDX-License-Identifier: EUPL-1.2 package main import ( "bytes" _ "embed" "encoding/json" "errors" "fmt" "io/fs" "mime" "path/filepath" "sort" "strings" "time" "github.com/tidwall/gjson" "gitroot.dev/libs/golang/plugin/model" ) const cachePath string = "worktree" type gitWorktreeLine struct { FullPath string Message string LastEdited time.Time Author string hasBeenUpdated bool } type worktree struct { p *Plugin lines []gitWorktreeLine } func (p *Plugin) LoadWorktree() { res := []gitWorktreeLine{} cache, err := fs.ReadFile(p.server.Cache(), cachePath) if err != nil && errors.Is(err, fs.ErrNotExist) { p.server.Log("cache not existing") } else if err != nil { p.server.LogError("can't read cache in LoadWorktree", err) } else { gjson.ForEachLine(string(cache), func(line gjson.Result) bool { res = append(res, unmarshalGitWorktreeLine(line)) return true }) } p.gitWorktree = &worktree{p: p, lines: res} } func (p *Plugin) StoreWorktree() { var buf bytes.Buffer for _, line := range p.gitWorktree.lines { buf.Write(marshalGitWorktreeLine(line)) buf.WriteString("\n") } p.server.ModifyCacheContent(cachePath, buf.String()) } func marshalGitWorktreeLine(line gitWorktreeLine) []byte { j, _ := json.Marshal(line) return j } func unmarshalGitWorktreeLine(line gjson.Result) gitWorktreeLine { return gitWorktreeLine{ FullPath: line.Get("FullPath").String(), Message: line.Get("Message").String(), LastEdited: line.Get("LastEdited").Time(), Author: line.Get("Author").String(), hasBeenUpdated: false, } } func (w *worktree) addOrModFile(path string, commit model.Commit) { w.delFile(path) w.lines = append(w.lines, gitWorktreeLine{ FullPath: path, Message: replaceEmoji(commit.Message), LastEdited: commit.Date, Author: "// TODO", hasBeenUpdated: true, }) } func (w *worktree) delFile(path string) { res := make([]gitWorktreeLine, 0) for _, line := range w.lines { if line.FullPath != path { res = append(res, line) } } w.lines = res } type worktreeLine struct { name string icon string targetLink string lastEdited time.Time author string message string isDir bool } func (w *worktree) preRender(dir string, prependDir string, finalizeRender func(fp string, htmlContent string)) []worktreeLine { res := make([]worktreeLine, 0) depthDir := strings.Count(dir, "/") dirToRender := make(map[string]any) for _, line := range w.lines { if strings.HasPrefix(line.FullPath, dir) { dirLine, filename := filepath.Split(line.FullPath) //dir if strings.Count(line.FullPath, "/") > depthDir { currentDir := strings.Split(strings.TrimPrefix(dirLine, dir), "/")[0] if _, ok := dirToRender[currentDir]; !ok { nextRenderPath := fmt.Sprintf("%s%s/", dir, currentDir) children := w.renderHtml(nextRenderPath, prependDir, finalizeRender) dirToRender[currentDir] = 1 if len(children) > 0 { lastCommit := children[0] for _, c := range children { if !c.isDir && c.lastEdited.Compare(lastCommit.lastEdited) > 0 { lastCommit = c } } res = append(res, worktreeLine{name: currentDir, icon: "📁", targetLink: fmt.Sprintf("%s/%sindex.html", prependDir, nextRenderPath), lastEdited: lastCommit.lastEdited, author: lastCommit.author, message: lastCommit.message, isDir: true}) } else { res = append(res, worktreeLine{name: currentDir, icon: "📁", targetLink: fmt.Sprintf("%s/%sindex.html", prependDir, nextRenderPath), lastEdited: line.LastEdited, author: line.Author, message: line.Message, isDir: true}) } } } //file if strings.Count(line.FullPath, "/") == depthDir { icon := getIcon(line.FullPath) if line.hasBeenUpdated { w.renderFile(line.FullPath, prependDir, finalizeRender) } res = append(res, worktreeLine{name: filename, icon: icon, targetLink: fmt.Sprintf("%s/%s%s.html", prependDir, dirLine, filename), lastEdited: line.LastEdited, author: line.Author, message: line.Message, isDir: false}) } } } return res } func (w *worktree) renderHtml(dir string, prependDir string, finalizeRender func(fp string, htmlContent string)) []worktreeLine { // early return if nothing has change for this dir atLeastOneChange := false for _, line := range w.lines { atLeastOneChange = line.hasBeenUpdated && strings.HasPrefix(line.FullPath, dir) if atLeastOneChange { break } } if !atLeastOneChange { return []worktreeLine{} } fullFilePath := filepath.Join(prependDir, dir) var buf bytes.Buffer buf.WriteString(renderBreadcrump(fullFilePath, false)) buf.WriteString("") lines := w.preRender(dir, prependDir, finalizeRender) sortedLines := byPath(lines) sort.Sort(sortedLines) for _, line := range lines { buf.WriteString("") msg := strings.SplitN(line.message, "\n", 2) comMsg := msg[0] details := "" if len(msg) > 1 { details = line.message } buf.WriteString(fmt.Sprintf("", line.icon, line.targetLink, line.name, details, comMsg, line.lastEdited.Format("02-01-2006"))) buf.WriteString("") } buf.WriteString("
NameMessageLast modified
%s%s%s%s
") finalizeRender(fmt.Sprintf("%s/index.html", fullFilePath), buf.String()) return lines } type byPath []worktreeLine func (a byPath) Len() int { return len(a) } func (a byPath) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a byPath) Less(i, j int) bool { if a[i].isDir == a[j].isDir { return strings.Compare(a[i].name, a[j].name) < 0 } return a[i].isDir } func (w *worktree) renderFile(path string, prependDir string, finalizeRender func(fp string, htmlContent string)) { _, filename := filepath.Split(path) ext := filepath.Ext(filename) typeMime := mime.TypeByExtension(ext) buf := bytes.NewBufferString("") if strings.HasPrefix(typeMime, "text/") || filename == "allowed_signers" || filename == "makefile" { content, err := fs.ReadFile(w.p.server.Worktree(), path) if err != nil { w.p.server.LogError("can't reafile "+path, err) } buf.WriteString(renderBreadcrump(filepath.Join(prependDir, path), true)) if code, ok := hilightByExt[ext]; ok { ext = code } code := string(content) lang := string(ext) if w.p.canCallCodePlugin { htmlCode, err := w.p.server.CallFunc(PLUGIN_CODE, PLUGIN_CODE_FUNC, map[string]string{"code": code, "lang": lang}) if err != nil { w.p.server.LogError("renderCode call renderCode fail", err) return } fmt.Fprint(buf, htmlCode["html"]) } else { htmlCode := fmt.Sprintf("
%s
", code) fmt.Fprint(buf, htmlCode) } } else if strings.HasPrefix(typeMime, "image/") || ext == ".svg" { buf.WriteString(renderBreadcrump(filepath.Join(prependDir, path), true)) buf.WriteString("
") } else { buf.WriteString(renderBreadcrump(filepath.Join(prependDir, path), true)) buf.WriteString("
Binary
") } finalizeRender(filepath.Join(prependDir, fmt.Sprintf("%s.html", path)), buf.String()) } func renderBreadcrump(fullFilePath string, isFile bool) string { var buf bytes.Buffer buf.WriteString("") return buf.String() }