package main import ( "bytes" "fmt" "html/template" "net/http" "strings" "github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2/formatters/html" "github.com/alecthomas/chroma/v2/lexers" "github.com/alecthomas/chroma/v2/styles" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/filemode" "github.com/go-git/go-git/v5/plumbing/object" "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" ) func highlight(filename, content string) template.HTML { lexer := lexers.Get(filename) if lexer == nil { lexer = lexers.Analyse(content) } if lexer == nil { lexer = lexers.Fallback } lexer = chroma.Coalesce(lexer) style := styles.Get("vs") if style == nil { style = styles.Fallback } formatter := html.New(html.WithLineNumbers(true), html.WithLinkableLineNumbers(true, "L")) iterator, err := lexer.Tokenise(nil, content) if err != nil { return template.HTML(template.HTMLEscapeString(content)) } var sb strings.Builder err = formatter.Format(&sb, style, iterator) if err != nil { return template.HTML(template.HTMLEscapeString(content)) } return template.HTML(sb.String()) } func scanMarkers(ctx *RepoContext) ([]Marker, error) { commit, err := ctx.GitRepo.CommitObject(ctx.Hash) if err != nil { return nil, err } tree, err := commit.Tree() if err != nil { return nil, err } var markers []Marker err = tree.Files().ForEach(func(f *object.File) error { // Skip vendor directory and binary files if strings.HasPrefix(f.Name, "vendor/") || strings.Contains(f.Name, "/vendor/") { return nil } if f.Mode.IsFile() && f.Size > 1024*1024 { // Skip files > 1MB return nil } isSource := false exts := []string{".go", ".js", ".ts", ".py", ".c", ".h", ".cpp", ".hpp", ".css", ".html", ".md", ".txt", ".yaml", ".yml", ".sh"} for _, ext := range exts { if strings.HasSuffix(strings.ToLower(f.Name), ext) { isSource = true break } } if !isSource { return nil } content, err := f.Contents() if err != nil { return nil } lines := strings.Split(content, "\n") for i, line := range lines { trimmed := strings.TrimSpace(line) markerType := "" if strings.Contains(trimmed, "TODO:") { markerType = "TODO" } else if strings.Contains(trimmed, "FIXME:") { markerType = "FIXME" } if markerType != "" { // Extract content after the marker idx := strings.Index(trimmed, markerType+":") content := strings.TrimSpace(trimmed[idx+len(markerType)+1:]) markers = append(markers, Marker{ Type: markerType, FilePath: f.Name, Line: i + 1, Content: content, }) } } return nil }) return markers, err } func getRepoContext(w http.ResponseWriter, r *http.Request) (*RepoContext, error) { name := r.PathValue("name") config := GlobalConfig var repo *Repository for _, repoItem := range config.Repositories { if repoItem.Name == name { repo = &repoItem break } } if repo == nil { return nil, fmt.Errorf("repository not found") } gitRepo, err := git.PlainOpen(repo.Path) if err != nil { return nil, fmt.Errorf("error opening repository: %w", err) } refName := r.URL.Query().Get("ref") var hash plumbing.Hash if refName == "" { head, err := gitRepo.Head() if err != nil { return nil, fmt.Errorf("error getting HEAD: %w", err) } hash = head.Hash() refName = head.Name().Short() } else { h, err := gitRepo.ResolveRevision(plumbing.Revision(refName)) if err != nil { return nil, fmt.Errorf("error resolving revision: %w", err) } hash = *h } metadataKey := name + ":" + hash.String() if val, ok := repoMetadataCache.Load(metadataKey); ok { meta := val.(RepoMetadata) if meta.Version >= CurrentMetadataVersion { return &RepoContext{ Repo: repo, GitRepo: gitRepo, Config: config, CurrentRef: refName, Hash: hash, Branches: meta.Branches, Tags: meta.Tags, ReadmeName: meta.ReadmeName, LicenseName: meta.LicenseName, }, nil } } // Get all branch-like references (local and remote) var branches []string rIter, err := gitRepo.References() seenBranches := make(map[string]bool) if err == nil { rIter.ForEach(func(r *plumbing.Reference) error { if r.Name().IsBranch() || r.Name().IsRemote() { name := r.Name().Short() if name == "origin/HEAD" || seenBranches[name] { return nil } branches = append(branches, name) seenBranches[name] = true } return nil }) } // Get tags var tags []string tIter, err := gitRepo.Tags() if err == nil { tIter.ForEach(func(r *plumbing.Reference) error { tags = append(tags, r.Name().Short()) return nil }) } // Detect README and LICENSE readmeName := "" licenseName := "" commit, err := gitRepo.CommitObject(hash) if err == nil { tree, err := commit.Tree() if err == nil { for _, entry := range tree.Entries { nameLower := strings.ToLower(entry.Name) if readmeName == "" && (nameLower == "readme.md" || nameLower == "readme.markdown" || nameLower == "readme") { readmeName = entry.Name } if licenseName == "" && (nameLower == "license" || nameLower == "license.md" || nameLower == "license.txt" || nameLower == "licence" || nameLower == "licence.md" || nameLower == "licence.txt" || nameLower == "copying" || nameLower == "copying.md" || nameLower == "copying.txt") { licenseName = entry.Name } if readmeName != "" && licenseName != "" { break } } } } // Note: totalCommits will be updated in repoHandler if needed, // for now we store what we have. repoMetadataCache.Store(metadataKey, RepoMetadata{ Branches: branches, Tags: tags, ReadmeName: readmeName, LicenseName: licenseName, Version: CurrentMetadataVersion, }) NotifySave() return &RepoContext{ Repo: repo, GitRepo: gitRepo, Config: config, CurrentRef: refName, Hash: hash, Branches: branches, Tags: tags, ReadmeName: readmeName, LicenseName: licenseName, }, nil } func formatMode(m filemode.FileMode) string { if m == filemode.Empty { return "----------" } s := m.String() switch m { case filemode.Regular: return "-rw-r--r--" case filemode.Executable: return "-rwxr-xr-x" case filemode.Dir: return "drwxr-xr-x" case filemode.Symlink: return "lrwxrwxrwx" default: return s } } func renderMarkdown(content string) template.HTML { md := goldmark.New( goldmark.WithExtensions( extension.GFM, extension.Linkify, extension.Table, AlertExtension, ), goldmark.WithParserOptions( parser.WithAutoHeadingID(), ), ) var buf bytes.Buffer if err := md.Convert([]byte(content), &buf); err != nil { return template.HTML(content) } return template.HTML(buf.String()) }