diff --git a/cache.go b/cache.go index 88bc75898638bf34c7c8c3427af9403d71a119e2..4573721422ae66258860273d1e4a26078caaf5d7 100644 --- a/cache.go +++ b/cache.go @@ -24,7 +24,6 @@ } const CurrentMetadataVersion = 1 - type LangCacheKey struct { RepoName string CommitHash string diff --git a/gitutils.go b/gitutils.go index 9eed64c88f054e90778ecf8512c6e1d612b34418..ab33338952bef3005e1f5872547bf2d970aca668 100644 --- a/gitutils.go +++ b/gitutils.go @@ -157,7 +157,6 @@ } hash = *h } - // Cache keys metadataKey := name + ":" + hash.String() if val, ok := repoMetadataCache.Load(metadataKey); ok { @@ -227,7 +226,7 @@ } } } - // Note: totalCommits will be updated in repoHandler if needed, + // Note: totalCommits will be updated in repoHandler if needed, // for now we store what we have. repoMetadataCache.Store(metadataKey, RepoMetadata{ Branches: branches, diff --git a/handlers.go b/handlers.go deleted file mode 100644 index 7c6c792bb26d8ff692829aa9bc42b07d09a8cd5d..0000000000000000000000000000000000000000 --- a/handlers.go +++ /dev/null @@ -1,992 +0,0 @@ -package main - -import ( - "archive/tar" - "compress/gzip" - "encoding/xml" - "fmt" - "html/template" - "io" - "log" - "net/http" - "path" - "sort" - "strconv" - "strings" - "sync" - "time" - - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/format/diff" - "github.com/go-git/go-git/v5/plumbing/object" -) - -func homeHandler(w http.ResponseWriter, r *http.Request) { - config := GlobalConfig - - groupsMap := make(map[string][]Repository) - var groupOrder []string - - for _, repo := range config.Repositories { - if _, ok := groupsMap[repo.Group]; !ok { - groupOrder = append(groupOrder, repo.Group) - } - groupsMap[repo.Group] = append(groupsMap[repo.Group], repo) - } - - var grouped []GroupedRepositories - for _, groupName := range groupOrder { - grouped = append(grouped, GroupedRepositories{ - Name: groupName, - Repositories: groupsMap[groupName], - }) - } - - err := templates.ExecuteTemplate(w, "repositories.html", struct { - Groups []GroupedRepositories - Repo *Repository - }{ - Groups: grouped, - Repo: nil, - }) - - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} - -func repoHandler(w http.ResponseWriter, r *http.Request) { - ctx, err := getRepoContext(w, r) - if err != nil { - if err.Error() == "repository not found" { - http.NotFound(w, r) - } else { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - return - } - - page, _ := strconv.Atoi(r.URL.Query().Get("page")) - if page < 1 { - page = 1 - } - pageSize := 30 - - totalCommitsKey := ctx.Repo.Name + ":" + ctx.Hash.String() - var totalCommits int - if val, ok := repoMetadataCache.Load(totalCommitsKey); ok { - totalCommits = val.(RepoMetadata).TotalCommits - } else { - cIter, err := ctx.GitRepo.Log(&git.LogOptions{From: ctx.Hash}) - if err != nil { - http.Error(w, fmt.Sprintf("Error getting log: %v", err), http.StatusInternalServerError) - return - } - _ = cIter.ForEach(func(c *object.Commit) error { - totalCommits++ - return nil - }) - repoMetadataCache.Store(totalCommitsKey, RepoMetadata{ - TotalCommits: totalCommits, - Branches: ctx.Branches, - Tags: ctx.Tags, - ReadmeName: ctx.ReadmeName, - LicenseName: ctx.LicenseName, - Version: CurrentMetadataVersion, - }) - NotifySave() - } - - totalPages := (totalCommits + pageSize - 1) / pageSize - - cIter, err := ctx.GitRepo.Log(&git.LogOptions{From: ctx.Hash}) - if err != nil { - http.Error(w, fmt.Sprintf("Error getting log: %v", err), http.StatusInternalServerError) - return - } - - var commits []Commit - var wg sync.WaitGroup - count := 0 - - // Collect commits for the current page - var commitsToProcess []*object.Commit - err = cIter.ForEach(func(c *object.Commit) error { - if count < (page-1)*pageSize { - count++ - return nil - } - if len(commitsToProcess) >= pageSize { - return fmt.Errorf("limit reached") - } - commitsToProcess = append(commitsToProcess, c) - count++ - return nil - }) - - if err != nil && err.Error() != "limit reached" { - http.Error(w, fmt.Sprintf("Error iterating commits: %v", err), http.StatusInternalServerError) - return - } - - commits = make([]Commit, len(commitsToProcess)) - for i, c := range commitsToProcess { - wg.Add(1) - go func(idx int, commit *object.Commit) { - defer wg.Done() - hashStr := commit.Hash.String() - - var adds, dels int - if val, ok := commitStatsCache.Load(hashStr); ok { - s := val.(CommitStat) - adds, dels = s.Additions, s.Deletions - } else { - stats, err := commit.Stats() - if err == nil { - for _, st := range stats { - adds += st.Addition - dels += st.Deletion - } - commitStatsCache.Store(hashStr, CommitStat{Additions: adds, Deletions: dels}) - NotifySave() - } - } - - commits[idx] = Commit{ - Hash: hashStr, - AuthorName: commit.Author.Name, - AuthorEmail: commit.Author.Email, - AuthorDate: commit.Author.When, - CommitterName: commit.Committer.Name, - CommitterEmail: commit.Committer.Email, - CommitterDate: commit.Committer.When, - Message: commit.Message, - Additions: adds, - Deletions: dels, - } - }(i, c) - } - wg.Wait() - - // Calculate page range (show up to 8 pages around current page) - startPage := page - 4 - if startPage < 1 { - startPage = 1 - } - endPage := startPage + 7 - if endPage > totalPages { - endPage = totalPages - startPage = endPage - 7 - if startPage < 1 { - startPage = 1 - } - } - - var pages []int - for i := startPage; i <= endPage; i++ { - pages = append(pages, i) - } - - commit, _ := ctx.GitRepo.CommitObject(ctx.Hash) - tree, _ := commit.Tree() - langStats, _ := getLanguageStats(ctx.Repo.Name, ctx.Hash.String(), tree) - - data := struct { - *RepoContext - Commits []Commit - Languages []LanguageStat - View string - Page int - TotalPages int - Pages []int - PrevPage int - NextPage int - }{ - RepoContext: ctx, - Commits: commits, - Languages: langStats, - View: "commits", - Page: page, - TotalPages: totalPages, - Pages: pages, - PrevPage: page - 1, - } - if page < totalPages { - data.NextPage = page + 1 - } - - err = templates.ExecuteTemplate(w, "repository.html", data) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} - -func treeHandler(w http.ResponseWriter, r *http.Request) { - ctx, err := getRepoContext(w, r) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - path := r.PathValue("path") - commit, err := ctx.GitRepo.CommitObject(ctx.Hash) - if err != nil { - http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError) - return - } - - tree, err := commit.Tree() - if err != nil { - http.Error(w, fmt.Sprintf("Error getting tree: %v", err), http.StatusInternalServerError) - return - } - - if path != "" { - tree, err = tree.Tree(path) - if err != nil { - http.NotFound(w, r) - return - } - } - - var entries []TreeEntry - for _, entry := range tree.Entries { - fullPath := entry.Name - if path != "" { - fullPath = path + "/" + entry.Name - } - - isDir := entry.Mode.IsFile() == false - - var size int64 - if !isDir { - obj, _ := ctx.GitRepo.Object(plumbing.AnyObject, entry.Hash) - if blob, ok := obj.(*object.Blob); ok { - size = blob.Size - } - } - - entries = append(entries, TreeEntry{ - Name: entry.Name, - Path: fullPath, - IsDir: isDir, - Size: size, - Mode: entry.Mode.String(), - }) - } - - sort.Slice(entries, func(i, j int) bool { - if entries[i].IsDir != entries[j].IsDir { - return entries[i].IsDir - } - return entries[i].Name < entries[j].Name - }) - - data := struct { - *RepoContext - Entries []TreeEntry - Path string - View string - }{ - RepoContext: ctx, - Entries: entries, - Path: path, - View: "tree", - } - - err = templates.ExecuteTemplate(w, "tree.html", data) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} - -func blobHandler(w http.ResponseWriter, r *http.Request) { - ctx, err := getRepoContext(w, r) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - path := r.PathValue("path") - commit, err := ctx.GitRepo.CommitObject(ctx.Hash) - if err != nil { - http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError) - return - } - - file, err := commit.File(path) - if err != nil { - http.NotFound(w, r) - return - } - - content, err := file.Contents() - if err != nil { - http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError) - return - } - - data := struct { - *RepoContext - Path string - Content template.HTML - }{ - RepoContext: ctx, - Path: path, - Content: highlight(path, content), - } - - err = templates.ExecuteTemplate(w, "blob.html", data) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} - -func rawHandler(w http.ResponseWriter, r *http.Request) { - ctx, err := getRepoContext(w, r) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - path := r.PathValue("path") - commit, err := ctx.GitRepo.CommitObject(ctx.Hash) - if err != nil { - http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError) - return - } - - file, err := commit.File(path) - if err != nil { - http.NotFound(w, r) - return - } - - reader, err := file.Reader() - if err != nil { - http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError) - return - } - defer reader.Close() - - w.Header().Set("Content-Type", "application/octet-stream") - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", path)) - io.Copy(w, reader) -} - -func archiveHandler(w http.ResponseWriter, r *http.Request) { - ctx, err := getRepoContext(w, r) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - pathValue := r.PathValue("path") - commit, err := ctx.GitRepo.CommitObject(ctx.Hash) - if err != nil { - http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError) - return - } - - tree, err := commit.Tree() - if err != nil { - http.Error(w, fmt.Sprintf("Error getting tree: %v", err), http.StatusInternalServerError) - return - } - - if pathValue != "" { - tree, err = tree.Tree(pathValue) - if err != nil { - http.NotFound(w, r) - return - } - } - - filename := ctx.Repo.Name - if pathValue != "" { - filename = path.Base(pathValue) - } - filename = fmt.Sprintf("%s-%s.tar.gz", filename, ctx.CurrentRef) - - w.Header().Set("Content-Type", "application/gzip") - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) - - gw := gzip.NewWriter(w) - defer gw.Close() - - tw := tar.NewWriter(gw) - defer tw.Close() - - err = tree.Files().ForEach(func(f *object.File) error { - hdr := &tar.Header{ - Name: strings.TrimPrefix(strings.TrimPrefix(f.Name, pathValue), "/"), - Mode: int64(f.Mode), - Size: f.Size, - } - - if err := tw.WriteHeader(hdr); err != nil { - return err - } - - reader, err := f.Reader() - if err != nil { - return err - } - defer reader.Close() - - _, err = io.Copy(tw, reader) - return err - }) - - if err != nil { - log.Printf("Error creating archive: %v", err) - } -} - -func commitHandler(w http.ResponseWriter, r *http.Request) { - ctx, err := getRepoContext(w, r) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - commitHash := r.PathValue("hash") - hash := plumbing.NewHash(commitHash) - commit, err := ctx.GitRepo.CommitObject(hash) - if err != nil { - http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError) - return - } - - var fileDiffs []FileDiff - maxChanges := 0 - if commit.NumParents() > 0 { - parent, _ := commit.Parent(0) - patch, err := parent.Patch(commit) - if err == nil { - for _, fp := range patch.FilePatches() { - from, to := fp.Files() - name := "" - mode := "" - if to != nil { - name = to.Path() - mode = formatMode(to.Mode()) - } else if from != nil { - name = from.Path() - mode = formatMode(from.Mode()) - } - - fileAdd, fileDel := 0, 0 - isBinary := fp.IsBinary() - deleted := to == nil - var oldSize, newSize int64 - - if isBinary { - if from != nil { - obj, _ := ctx.GitRepo.Object(plumbing.AnyObject, from.Hash()) - if blob, ok := obj.(*object.Blob); ok { - oldSize = blob.Size - } - } - if to != nil { - obj, _ := ctx.GitRepo.Object(plumbing.AnyObject, to.Hash()) - if blob, ok := obj.(*object.Blob); ok { - newSize = blob.Size - } - } - } - - var diffLines []DiffLine - leftNo, rightNo := 1, 1 - - var delLines []string - var addLines []string - - flush := func() { - max := len(delLines) - if len(addLines) > max { - max = len(addLines) - } - for i := 0; i < max; i++ { - line := DiffLine{} - if i < len(delLines) && i < len(addLines) { - line.LeftNo = fmt.Sprintf("%d", leftNo) - line.Left = delLines[i] - line.RightNo = fmt.Sprintf("%d", rightNo) - line.Right = addLines[i] - line.Type = "mod" - leftNo++ - rightNo++ - fileAdd++ - fileDel++ - } else if i < len(delLines) { - line.LeftNo = fmt.Sprintf("%d", leftNo) - line.Left = delLines[i] - line.Type = "del" - leftNo++ - fileDel++ - } else if i < len(addLines) { - line.RightNo = fmt.Sprintf("%d", rightNo) - line.Right = addLines[i] - line.Type = "add" - rightNo++ - fileAdd++ - } - diffLines = append(diffLines, line) - } - delLines = nil - addLines = nil - } - - for _, chunk := range fp.Chunks() { - lines := strings.Split(strings.TrimSuffix(chunk.Content(), "\n"), "\n") - switch chunk.Type() { - case diff.Equal: - flush() - for _, line := range lines { - diffLines = append(diffLines, DiffLine{ - LeftNo: fmt.Sprintf("%d", leftNo), - Left: line, - RightNo: fmt.Sprintf("%d", rightNo), - Right: line, - Type: "eq", - }) - leftNo++ - rightNo++ - } - case diff.Delete: - delLines = append(delLines, lines...) - case diff.Add: - addLines = append(addLines, lines...) - } - } - flush() - - if fileAdd+fileDel > maxChanges { - maxChanges = fileAdd + fileDel - } - - visible := make([]bool, len(diffLines)) - for i, line := range diffLines { - if line.Type == "add" || line.Type == "del" || line.Type == "mod" { - for j := i - 3; j <= i+3; j++ { - if j >= 0 && j < len(diffLines) { - visible[j] = true - } - } - } - } - - var filteredLines []DiffLine - lastWasGap := false - for i, isVisible := range visible { - if isVisible { - filteredLines = append(filteredLines, diffLines[i]) - lastWasGap = false - } else { - if !lastWasGap { - filteredLines = append(filteredLines, DiffLine{Type: "gap"}) - lastWasGap = true - } - } - } - - fileDiffs = append(fileDiffs, FileDiff{ - Name: name, - Lines: filteredLines, - Addition: fileAdd, - Deletion: fileDel, - IsBinary: isBinary, - Mode: mode, - OldSize: oldSize, - NewSize: newSize, - Deleted: deleted, - }) - } - } - } - - stats, _ := commit.Stats() - adds, dels := 0, 0 - for _, s := range stats { - adds += s.Addition - dels += s.Deletion - } - - data := struct { - *RepoContext - Commit Commit - FileDiffs []FileDiff - MaxChanges int - }{ - RepoContext: ctx, - Commit: Commit{ - Hash: commit.Hash.String(), - AuthorName: commit.Author.Name, - AuthorEmail: commit.Author.Email, - AuthorDate: commit.Author.When, - CommitterName: commit.Committer.Name, - CommitterEmail: commit.Committer.Email, - CommitterDate: commit.Committer.When, - Message: commit.Message, - Additions: adds, - Deletions: dels, - }, - FileDiffs: fileDiffs, - MaxChanges: maxChanges, - } - - err = templates.ExecuteTemplate(w, "commit.html", data) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} - -func patchHandler(w http.ResponseWriter, r *http.Request) { - repoName := r.PathValue("name") - commitHash := r.PathValue("hash") - - config, err := loadConfig(ConfigPath) - if err != nil { - http.Error(w, fmt.Sprintf("Error loading config: %v", err), http.StatusInternalServerError) - return - } - - var repo *Repository - for _, repoItem := range config.Repositories { - if repoItem.Name == repoName { - repo = &repoItem - break - } - } - - if repo == nil { - http.NotFound(w, r) - return - } - - gitRepo, err := git.PlainOpen(repo.Path) - if err != nil { - http.Error(w, fmt.Sprintf("Error opening repository: %v", err), http.StatusInternalServerError) - return - } - - hash := plumbing.NewHash(commitHash) - commit, err := gitRepo.CommitObject(hash) - if err != nil { - http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "text/plain") - - currentTree, err := commit.Tree() - if err != nil { - http.Error(w, fmt.Sprintf("Error getting tree: %v", err), http.StatusInternalServerError) - return - } - - var parentTree *object.Tree - if commit.NumParents() > 0 { - parent, _ := commit.Parent(0) - parentTree, err = parent.Tree() - if err != nil { - http.Error(w, fmt.Sprintf("Error getting parent tree: %v", err), http.StatusInternalServerError) - return - } - } - - patch, err := parentTree.Patch(currentTree) - if err != nil { - http.Error(w, fmt.Sprintf("Error generating patch: %v", err), http.StatusInternalServerError) - return - } - fmt.Fprint(w, patch.String()) -} - -func readmeHandler(w http.ResponseWriter, r *http.Request) { - ctx, err := getRepoContext(w, r) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - if ctx.ReadmeName == "" { - http.NotFound(w, r) - return - } - - commit, err := ctx.GitRepo.CommitObject(ctx.Hash) - if err != nil { - http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError) - return - } - - file, err := commit.File(ctx.ReadmeName) - if err != nil { - http.NotFound(w, r) - return - } - - content, err := file.Contents() - if err != nil { - http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError) - return - } - - data := struct { - *RepoContext - Content template.HTML - }{ - RepoContext: ctx, - Content: renderMarkdown(content), - } - - err = templates.ExecuteTemplate(w, "readme.html", data) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} - -func licenseHandler(w http.ResponseWriter, r *http.Request) { - ctx, err := getRepoContext(w, r) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - if ctx.LicenseName == "" { - http.NotFound(w, r) - return - } - - commit, err := ctx.GitRepo.CommitObject(ctx.Hash) - if err != nil { - http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError) - return - } - - file, err := commit.File(ctx.LicenseName) - if err != nil { - http.NotFound(w, r) - return - } - - content, err := file.Contents() - if err != nil { - http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError) - return - } - - data := struct { - *RepoContext - Content template.HTML - }{ - RepoContext: ctx, - Content: renderMarkdown(content), - } - - err = templates.ExecuteTemplate(w, "license.html", data) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} - -func markersHandler(w http.ResponseWriter, r *http.Request) { - ctx, err := getRepoContext(w, r) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - markers, err := scanMarkers(ctx) - if err != nil { - http.Error(w, fmt.Sprintf("Error scanning markers: %v", err), http.StatusInternalServerError) - return - } - - data := struct { - *RepoContext - Markers []Marker - }{ - RepoContext: ctx, - Markers: markers, - } - - err = templates.ExecuteTemplate(w, "markers.html", data) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} - -func repoCommitsRSSHandler(w http.ResponseWriter, r *http.Request) { - ctx, err := getRepoContext(w, r) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - cIter, err := ctx.GitRepo.Log(&git.LogOptions{From: ctx.Hash}) - if err != nil { - http.Error(w, fmt.Sprintf("Error getting log: %v", err), http.StatusInternalServerError) - return - } - - scheme := "http" - if r.TLS != nil { - scheme = "https" - } - baseURL := fmt.Sprintf("%s://%s", scheme, r.Host) - - rss := RSS{ - Version: "2.0", - Channel: Channel{ - Title: fmt.Sprintf("%s - Commits", ctx.Repo.Name), - Link: fmt.Sprintf("%s/r/%s?ref=%s", baseURL, ctx.Repo.Name, ctx.CurrentRef), - Description: fmt.Sprintf("Commit history for %s (%s)", ctx.Repo.Name, ctx.CurrentRef), - }, - } - - count := 0 - err = cIter.ForEach(func(c *object.Commit) error { - if count >= 20 { - return fmt.Errorf("limit reached") - } - - hash := c.Hash.String() - item := RSSItem{ - Title: strings.Split(c.Message, "\n")[0], - Link: fmt.Sprintf("%s/r/%s/c/%s", baseURL, ctx.Repo.Name, hash), - Description: c.Message, - PubDate: c.Author.When.Format(time.RFC1123Z), - GUID: fmt.Sprintf("%s/r/%s/c/%s", baseURL, ctx.Repo.Name, hash), - } - rss.Channel.Items = append(rss.Channel.Items, item) - count++ - return nil - }) - - if err != nil && err.Error() != "limit reached" { - http.Error(w, fmt.Sprintf("Error iterating commits: %v", err), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/xml; charset=utf-8") - fmt.Fprint(w, xml.Header) - enc := xml.NewEncoder(w) - enc.Indent("", " ") - if err := enc.Encode(rss); err != nil { - log.Printf("Error encoding RSS: %v", err) - } -} - -func repoTagsRSSHandler(w http.ResponseWriter, r *http.Request) { - name := r.PathValue("name") - config := GlobalConfig - - var repo *Repository - for _, repoItem := range config.Repositories { - if repoItem.Name == name { - repo = &repoItem - break - } - } - - if repo == nil { - http.NotFound(w, r) - return - } - - gitRepo, err := git.PlainOpen(repo.Path) - if err != nil { - http.Error(w, fmt.Sprintf("Error opening repository: %v", err), http.StatusInternalServerError) - return - } - - tIter, err := gitRepo.Tags() - if err != nil { - http.Error(w, fmt.Sprintf("Error getting tags: %v", err), http.StatusInternalServerError) - return - } - - type tagInfo struct { - Name string - Date time.Time - Hash string - } - var tags []tagInfo - - err = tIter.ForEach(func(ref *plumbing.Reference) error { - obj, err := gitRepo.TagObject(ref.Hash()) - if err != nil { - // Lightweight tag - commit, err := gitRepo.CommitObject(ref.Hash()) - if err == nil { - tags = append(tags, tagInfo{ - Name: ref.Name().Short(), - Date: commit.Author.When, - Hash: ref.Hash().String(), - }) - } - } else { - // Annotated tag - if _, err := obj.Commit(); err == nil { - tags = append(tags, tagInfo{ - Name: ref.Name().Short(), - Date: obj.Tagger.When, - Hash: ref.Hash().String(), - }) - } else { - tags = append(tags, tagInfo{ - Name: ref.Name().Short(), - Date: obj.Tagger.When, - Hash: obj.Target.String(), - }) - } - } - return nil - }) - - sort.Slice(tags, func(i, j int) bool { - return tags[i].Date.After(tags[j].Date) - }) - - if len(tags) > 20 { - tags = tags[:20] - } - - scheme := "http" - if r.TLS != nil { - scheme = "https" - } - baseURL := fmt.Sprintf("%s://%s", scheme, r.Host) - - rss := RSS{ - Version: "2.0", - Channel: Channel{ - Title: fmt.Sprintf("%s - Tags", repo.Name), - Link: fmt.Sprintf("%s/r/%s", baseURL, repo.Name), - Description: fmt.Sprintf("Tags for %s", repo.Name), - }, - } - - for _, t := range tags { - item := RSSItem{ - Title: t.Name, - Link: fmt.Sprintf("%s/r/%s?ref=%s", baseURL, repo.Name, t.Name), - Description: fmt.Sprintf("Tag %s at %s", t.Name, t.Hash), - PubDate: t.Date.Format(time.RFC1123Z), - GUID: fmt.Sprintf("%s/r/%s/tags/%s", baseURL, repo.Name, t.Name), - } - rss.Channel.Items = append(rss.Channel.Items, item) - } - - w.Header().Set("Content-Type", "application/xml; charset=utf-8") - fmt.Fprint(w, xml.Header) - enc := xml.NewEncoder(w) - enc.Indent("", " ") - if err := enc.Encode(rss); err != nil { - log.Printf("Error encoding RSS: %v", err) - } -} diff --git a/hcommit.go b/hcommit.go new file mode 100644 index 0000000000000000000000000000000000000000..fd459b8b314533b3d5b9c2648e66cd285db63729 --- /dev/null +++ b/hcommit.go @@ -0,0 +1,273 @@ +package main + +import ( + "fmt" + "net/http" + "strings" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/format/diff" + "github.com/go-git/go-git/v5/plumbing/object" +) + +func commitHandler(w http.ResponseWriter, r *http.Request) { + ctx, err := getRepoContext(w, r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + commitHash := r.PathValue("hash") + hash := plumbing.NewHash(commitHash) + commit, err := ctx.GitRepo.CommitObject(hash) + if err != nil { + http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError) + return + } + + var fileDiffs []FileDiff + maxChanges := 0 + if commit.NumParents() > 0 { + parent, _ := commit.Parent(0) + patch, err := parent.Patch(commit) + if err == nil { + for _, fp := range patch.FilePatches() { + from, to := fp.Files() + name := "" + mode := "" + if to != nil { + name = to.Path() + mode = formatMode(to.Mode()) + } else if from != nil { + name = from.Path() + mode = formatMode(from.Mode()) + } + + fileAdd, fileDel := 0, 0 + isBinary := fp.IsBinary() + deleted := to == nil + var oldSize, newSize int64 + + if isBinary { + if from != nil { + obj, _ := ctx.GitRepo.Object(plumbing.AnyObject, from.Hash()) + if blob, ok := obj.(*object.Blob); ok { + oldSize = blob.Size + } + } + if to != nil { + obj, _ := ctx.GitRepo.Object(plumbing.AnyObject, to.Hash()) + if blob, ok := obj.(*object.Blob); ok { + newSize = blob.Size + } + } + } + + var diffLines []DiffLine + leftNo, rightNo := 1, 1 + + var delLines []string + var addLines []string + + flush := func() { + max := len(delLines) + if len(addLines) > max { + max = len(addLines) + } + for i := 0; i < max; i++ { + line := DiffLine{} + if i < len(delLines) && i < len(addLines) { + line.LeftNo = fmt.Sprintf("%d", leftNo) + line.Left = delLines[i] + line.RightNo = fmt.Sprintf("%d", rightNo) + line.Right = addLines[i] + line.Type = "mod" + leftNo++ + rightNo++ + fileAdd++ + fileDel++ + } else if i < len(delLines) { + line.LeftNo = fmt.Sprintf("%d", leftNo) + line.Left = delLines[i] + line.Type = "del" + leftNo++ + fileDel++ + } else if i < len(addLines) { + line.RightNo = fmt.Sprintf("%d", rightNo) + line.Right = addLines[i] + line.Type = "add" + rightNo++ + fileAdd++ + } + diffLines = append(diffLines, line) + } + delLines = nil + addLines = nil + } + + for _, chunk := range fp.Chunks() { + lines := strings.Split(strings.TrimSuffix(chunk.Content(), "\n"), "\n") + switch chunk.Type() { + case diff.Equal: + flush() + for _, line := range lines { + diffLines = append(diffLines, DiffLine{ + LeftNo: fmt.Sprintf("%d", leftNo), + Left: line, + RightNo: fmt.Sprintf("%d", rightNo), + Right: line, + Type: "eq", + }) + leftNo++ + rightNo++ + } + case diff.Delete: + delLines = append(delLines, lines...) + case diff.Add: + addLines = append(addLines, lines...) + } + } + flush() + + if fileAdd+fileDel > maxChanges { + maxChanges = fileAdd + fileDel + } + + visible := make([]bool, len(diffLines)) + for i, line := range diffLines { + if line.Type == "add" || line.Type == "del" || line.Type == "mod" { + for j := i - 3; j <= i+3; j++ { + if j >= 0 && j < len(diffLines) { + visible[j] = true + } + } + } + } + + var filteredLines []DiffLine + lastWasGap := false + for i, isVisible := range visible { + if isVisible { + filteredLines = append(filteredLines, diffLines[i]) + lastWasGap = false + } else { + if !lastWasGap { + filteredLines = append(filteredLines, DiffLine{Type: "gap"}) + lastWasGap = true + } + } + } + + fileDiffs = append(fileDiffs, FileDiff{ + Name: name, + Lines: filteredLines, + Addition: fileAdd, + Deletion: fileDel, + IsBinary: isBinary, + Mode: mode, + OldSize: oldSize, + NewSize: newSize, + Deleted: deleted, + }) + } + } + } + + stats, _ := commit.Stats() + adds, dels := 0, 0 + for _, s := range stats { + adds += s.Addition + dels += s.Deletion + } + + data := struct { + *RepoContext + Commit Commit + FileDiffs []FileDiff + MaxChanges int + }{ + RepoContext: ctx, + Commit: Commit{ + Hash: commit.Hash.String(), + AuthorName: commit.Author.Name, + AuthorEmail: commit.Author.Email, + AuthorDate: commit.Author.When, + CommitterName: commit.Committer.Name, + CommitterEmail: commit.Committer.Email, + CommitterDate: commit.Committer.When, + Message: commit.Message, + Additions: adds, + Deletions: dels, + }, + FileDiffs: fileDiffs, + MaxChanges: maxChanges, + } + + err = templates.ExecuteTemplate(w, "commit.html", data) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func patchHandler(w http.ResponseWriter, r *http.Request) { + repoName := r.PathValue("name") + commitHash := r.PathValue("hash") + + config, err := loadConfig(ConfigPath) + if err != nil { + http.Error(w, fmt.Sprintf("Error loading config: %v", err), http.StatusInternalServerError) + return + } + + var repo *Repository + for _, repoItem := range config.Repositories { + if repoItem.Name == repoName { + repo = &repoItem + break + } + } + + if repo == nil { + http.NotFound(w, r) + return + } + + gitRepo, err := git.PlainOpen(repo.Path) + if err != nil { + http.Error(w, fmt.Sprintf("Error opening repository: %v", err), http.StatusInternalServerError) + return + } + + hash := plumbing.NewHash(commitHash) + commit, err := gitRepo.CommitObject(hash) + if err != nil { + http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/plain") + + currentTree, err := commit.Tree() + if err != nil { + http.Error(w, fmt.Sprintf("Error getting tree: %v", err), http.StatusInternalServerError) + return + } + + var parentTree *object.Tree + if commit.NumParents() > 0 { + parent, _ := commit.Parent(0) + parentTree, err = parent.Tree() + if err != nil { + http.Error(w, fmt.Sprintf("Error getting parent tree: %v", err), http.StatusInternalServerError) + return + } + } + + patch, err := parentTree.Patch(currentTree) + if err != nil { + http.Error(w, fmt.Sprintf("Error generating patch: %v", err), http.StatusInternalServerError) + return + } + fmt.Fprint(w, patch.String()) +} diff --git a/hhome.go b/hhome.go new file mode 100644 index 0000000000000000000000000000000000000000..e4b4d3ce567bd15f16aee478fc9a00ad11175353 --- /dev/null +++ b/hhome.go @@ -0,0 +1,39 @@ +package main + +import ( + "net/http" +) + +func homeHandler(w http.ResponseWriter, r *http.Request) { + config := GlobalConfig + + groupsMap := make(map[string][]Repository) + var groupOrder []string + + for _, repo := range config.Repositories { + if _, ok := groupsMap[repo.Group]; !ok { + groupOrder = append(groupOrder, repo.Group) + } + groupsMap[repo.Group] = append(groupsMap[repo.Group], repo) + } + + var grouped []GroupedRepositories + for _, groupName := range groupOrder { + grouped = append(grouped, GroupedRepositories{ + Name: groupName, + Repositories: groupsMap[groupName], + }) + } + + err := templates.ExecuteTemplate(w, "repositories.html", struct { + Groups []GroupedRepositories + Repo *Repository + }{ + Groups: grouped, + Repo: nil, + }) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} diff --git a/hinfo.go b/hinfo.go new file mode 100644 index 0000000000000000000000000000000000000000..10d782ab416fd8c8e89d54c6a2c470330734f407 --- /dev/null +++ b/hinfo.go @@ -0,0 +1,122 @@ +package main + +import ( + "fmt" + "html/template" + "net/http" +) + +func readmeHandler(w http.ResponseWriter, r *http.Request) { + ctx, err := getRepoContext(w, r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if ctx.ReadmeName == "" { + http.NotFound(w, r) + return + } + + commit, err := ctx.GitRepo.CommitObject(ctx.Hash) + if err != nil { + http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError) + return + } + + file, err := commit.File(ctx.ReadmeName) + if err != nil { + http.NotFound(w, r) + return + } + + content, err := file.Contents() + if err != nil { + http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError) + return + } + + data := struct { + *RepoContext + Content template.HTML + }{ + RepoContext: ctx, + Content: renderMarkdown(content), + } + + err = templates.ExecuteTemplate(w, "readme.html", data) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func licenseHandler(w http.ResponseWriter, r *http.Request) { + ctx, err := getRepoContext(w, r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if ctx.LicenseName == "" { + http.NotFound(w, r) + return + } + + commit, err := ctx.GitRepo.CommitObject(ctx.Hash) + if err != nil { + http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError) + return + } + + file, err := commit.File(ctx.LicenseName) + if err != nil { + http.NotFound(w, r) + return + } + + content, err := file.Contents() + if err != nil { + http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError) + return + } + + data := struct { + *RepoContext + Content template.HTML + }{ + RepoContext: ctx, + Content: renderMarkdown(content), + } + + err = templates.ExecuteTemplate(w, "license.html", data) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func markersHandler(w http.ResponseWriter, r *http.Request) { + ctx, err := getRepoContext(w, r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + markers, err := scanMarkers(ctx) + if err != nil { + http.Error(w, fmt.Sprintf("Error scanning markers: %v", err), http.StatusInternalServerError) + return + } + + data := struct { + *RepoContext + Markers []Marker + }{ + RepoContext: ctx, + Markers: markers, + } + + err = templates.ExecuteTemplate(w, "markers.html", data) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} diff --git a/hrepo.go b/hrepo.go new file mode 100644 index 0000000000000000000000000000000000000000..43e3d8c9af0627885bec73d1c36182255ff89a7c --- /dev/null +++ b/hrepo.go @@ -0,0 +1,353 @@ +package main + +import ( + "encoding/xml" + "fmt" + "log" + "net/http" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" +) + +func repoHandler(w http.ResponseWriter, r *http.Request) { + ctx, err := getRepoContext(w, r) + if err != nil { + if err.Error() == "repository not found" { + http.NotFound(w, r) + } else { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + return + } + + page, _ := strconv.Atoi(r.URL.Query().Get("page")) + if page < 1 { + page = 1 + } + pageSize := 30 + + totalCommitsKey := ctx.Repo.Name + ":" + ctx.Hash.String() + var totalCommits int + if val, ok := repoMetadataCache.Load(totalCommitsKey); ok { + totalCommits = val.(RepoMetadata).TotalCommits + } else { + cIter, err := ctx.GitRepo.Log(&git.LogOptions{From: ctx.Hash}) + if err != nil { + http.Error(w, fmt.Sprintf("Error getting log: %v", err), http.StatusInternalServerError) + return + } + _ = cIter.ForEach(func(c *object.Commit) error { + totalCommits++ + return nil + }) + repoMetadataCache.Store(totalCommitsKey, RepoMetadata{ + TotalCommits: totalCommits, + Branches: ctx.Branches, + Tags: ctx.Tags, + ReadmeName: ctx.ReadmeName, + LicenseName: ctx.LicenseName, + Version: CurrentMetadataVersion, + }) + NotifySave() + } + + totalPages := (totalCommits + pageSize - 1) / pageSize + + cIter, err := ctx.GitRepo.Log(&git.LogOptions{From: ctx.Hash}) + if err != nil { + http.Error(w, fmt.Sprintf("Error getting log: %v", err), http.StatusInternalServerError) + return + } + + var commits []Commit + var wg sync.WaitGroup + count := 0 + + var commitsToProcess []*object.Commit + err = cIter.ForEach(func(c *object.Commit) error { + if count < (page-1)*pageSize { + count++ + return nil + } + if len(commitsToProcess) >= pageSize { + return fmt.Errorf("limit reached") + } + commitsToProcess = append(commitsToProcess, c) + count++ + return nil + }) + + if err != nil && err.Error() != "limit reached" { + http.Error(w, fmt.Sprintf("Error iterating commits: %v", err), http.StatusInternalServerError) + return + } + + commits = make([]Commit, len(commitsToProcess)) + for i, c := range commitsToProcess { + wg.Add(1) + go func(idx int, commit *object.Commit) { + defer wg.Done() + hashStr := commit.Hash.String() + + var adds, dels int + if val, ok := commitStatsCache.Load(hashStr); ok { + s := val.(CommitStat) + adds, dels = s.Additions, s.Deletions + } else { + stats, err := commit.Stats() + if err == nil { + for _, st := range stats { + adds += st.Addition + dels += st.Deletion + } + commitStatsCache.Store(hashStr, CommitStat{Additions: adds, Deletions: dels}) + NotifySave() + } + } + + commits[idx] = Commit{ + Hash: hashStr, + AuthorName: commit.Author.Name, + AuthorEmail: commit.Author.Email, + AuthorDate: commit.Author.When, + CommitterName: commit.Committer.Name, + CommitterEmail: commit.Committer.Email, + CommitterDate: commit.Committer.When, + Message: commit.Message, + Additions: adds, + Deletions: dels, + } + }(i, c) + } + wg.Wait() + + // Calculate page range (show up to 8 pages around current page) + startPage := page - 4 + if startPage < 1 { + startPage = 1 + } + endPage := startPage + 7 + if endPage > totalPages { + endPage = totalPages + startPage = endPage - 7 + if startPage < 1 { + startPage = 1 + } + } + + var pages []int + for i := startPage; i <= endPage; i++ { + pages = append(pages, i) + } + + commit, _ := ctx.GitRepo.CommitObject(ctx.Hash) + tree, _ := commit.Tree() + langStats, _ := getLanguageStats(ctx.Repo.Name, ctx.Hash.String(), tree) + + data := struct { + *RepoContext + Commits []Commit + Languages []LanguageStat + View string + Page int + TotalPages int + Pages []int + PrevPage int + NextPage int + }{ + RepoContext: ctx, + Commits: commits, + Languages: langStats, + View: "commits", + Page: page, + TotalPages: totalPages, + Pages: pages, + PrevPage: page - 1, + } + if page < totalPages { + data.NextPage = page + 1 + } + + err = templates.ExecuteTemplate(w, "repository.html", data) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func repoCommitsRSSHandler(w http.ResponseWriter, r *http.Request) { + ctx, err := getRepoContext(w, r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + cIter, err := ctx.GitRepo.Log(&git.LogOptions{From: ctx.Hash}) + if err != nil { + http.Error(w, fmt.Sprintf("Error getting log: %v", err), http.StatusInternalServerError) + return + } + + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + baseURL := fmt.Sprintf("%s://%s", scheme, r.Host) + + rss := RSS{ + Version: "2.0", + Channel: Channel{ + Title: fmt.Sprintf("%s - Commits", ctx.Repo.Name), + Link: fmt.Sprintf("%s/r/%s?ref=%s", baseURL, ctx.Repo.Name, ctx.CurrentRef), + Description: fmt.Sprintf("Commit history for %s (%s)", ctx.Repo.Name, ctx.CurrentRef), + }, + } + + count := 0 + err = cIter.ForEach(func(c *object.Commit) error { + if count >= 20 { + return fmt.Errorf("limit reached") + } + + hash := c.Hash.String() + item := RSSItem{ + Title: strings.Split(c.Message, "\n")[0], + Link: fmt.Sprintf("%s/r/%s/c/%s", baseURL, ctx.Repo.Name, hash), + Description: c.Message, + PubDate: c.Author.When.Format(time.RFC1123Z), + GUID: fmt.Sprintf("%s/r/%s/c/%s", baseURL, ctx.Repo.Name, hash), + } + rss.Channel.Items = append(rss.Channel.Items, item) + count++ + return nil + }) + + if err != nil && err.Error() != "limit reached" { + http.Error(w, fmt.Sprintf("Error iterating commits: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/xml; charset=utf-8") + fmt.Fprint(w, xml.Header) + enc := xml.NewEncoder(w) + enc.Indent("", " ") + if err := enc.Encode(rss); err != nil { + log.Printf("Error encoding RSS: %v", err) + } +} + +func repoTagsRSSHandler(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("name") + config := GlobalConfig + + var repo *Repository + for _, repoItem := range config.Repositories { + if repoItem.Name == name { + repo = &repoItem + break + } + } + + if repo == nil { + http.NotFound(w, r) + return + } + + gitRepo, err := git.PlainOpen(repo.Path) + if err != nil { + http.Error(w, fmt.Sprintf("Error opening repository: %v", err), http.StatusInternalServerError) + return + } + + tIter, err := gitRepo.Tags() + if err != nil { + http.Error(w, fmt.Sprintf("Error getting tags: %v", err), http.StatusInternalServerError) + return + } + + type tagInfo struct { + Name string + Date time.Time + Hash string + } + var tags []tagInfo + + err = tIter.ForEach(func(ref *plumbing.Reference) error { + obj, err := gitRepo.TagObject(ref.Hash()) + if err != nil { + // Lightweight tag + commit, err := gitRepo.CommitObject(ref.Hash()) + if err == nil { + tags = append(tags, tagInfo{ + Name: ref.Name().Short(), + Date: commit.Author.When, + Hash: ref.Hash().String(), + }) + } + } else { + // Annotated tag + if _, err := obj.Commit(); err == nil { + tags = append(tags, tagInfo{ + Name: ref.Name().Short(), + Date: obj.Tagger.When, + Hash: ref.Hash().String(), + }) + } else { + tags = append(tags, tagInfo{ + Name: ref.Name().Short(), + Date: obj.Tagger.When, + Hash: obj.Target.String(), + }) + } + } + return nil + }) + + sort.Slice(tags, func(i, j int) bool { + return tags[i].Date.After(tags[j].Date) + }) + + if len(tags) > 20 { + tags = tags[:20] + } + + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + baseURL := fmt.Sprintf("%s://%s", scheme, r.Host) + + rss := RSS{ + Version: "2.0", + Channel: Channel{ + Title: fmt.Sprintf("%s - Tags", repo.Name), + Link: fmt.Sprintf("%s/r/%s", baseURL, repo.Name), + Description: fmt.Sprintf("Tags for %s", repo.Name), + }, + } + + for _, t := range tags { + item := RSSItem{ + Title: t.Name, + Link: fmt.Sprintf("%s/r/%s?ref=%s", baseURL, repo.Name, t.Name), + Description: fmt.Sprintf("Tag %s at %s", t.Name, t.Hash), + PubDate: t.Date.Format(time.RFC1123Z), + GUID: fmt.Sprintf("%s/r/%s/tags/%s", baseURL, repo.Name, t.Name), + } + rss.Channel.Items = append(rss.Channel.Items, item) + } + + w.Header().Set("Content-Type", "application/xml; charset=utf-8") + fmt.Fprint(w, xml.Header) + enc := xml.NewEncoder(w) + enc.Indent("", " ") + if err := enc.Encode(rss); err != nil { + log.Printf("Error encoding RSS: %v", err) + } +} diff --git a/htree.go b/htree.go new file mode 100644 index 0000000000000000000000000000000000000000..da4eb5db55637cc7fd44744a2e8557fe14af9520 --- /dev/null +++ b/htree.go @@ -0,0 +1,239 @@ +package main + +import ( + "archive/tar" + "compress/gzip" + "fmt" + "html/template" + "io" + "log" + "net/http" + "path" + "sort" + "strings" + + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" +) + +func treeHandler(w http.ResponseWriter, r *http.Request) { + ctx, err := getRepoContext(w, r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + pathValue := r.PathValue("path") + commit, err := ctx.GitRepo.CommitObject(ctx.Hash) + if err != nil { + http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError) + return + } + + tree, err := commit.Tree() + if err != nil { + http.Error(w, fmt.Sprintf("Error getting tree: %v", err), http.StatusInternalServerError) + return + } + + if pathValue != "" { + tree, err = tree.Tree(pathValue) + if err != nil { + http.NotFound(w, r) + return + } + } + + var entries []TreeEntry + for _, entry := range tree.Entries { + fullPath := entry.Name + if pathValue != "" { + fullPath = pathValue + "/" + entry.Name + } + + isDir := entry.Mode.IsFile() == false + + var size int64 + if !isDir { + obj, _ := ctx.GitRepo.Object(plumbing.AnyObject, entry.Hash) + if blob, ok := obj.(*object.Blob); ok { + size = blob.Size + } + } + + entries = append(entries, TreeEntry{ + Name: entry.Name, + Path: fullPath, + IsDir: isDir, + Size: size, + Mode: entry.Mode.String(), + }) + } + + sort.Slice(entries, func(i, j int) bool { + if entries[i].IsDir != entries[j].IsDir { + return entries[i].IsDir + } + return entries[i].Name < entries[j].Name + }) + + data := struct { + *RepoContext + Entries []TreeEntry + Path string + View string + }{ + RepoContext: ctx, + Entries: entries, + Path: pathValue, + View: "tree", + } + + err = templates.ExecuteTemplate(w, "tree.html", data) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func blobHandler(w http.ResponseWriter, r *http.Request) { + ctx, err := getRepoContext(w, r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + pathValue := r.PathValue("path") + commit, err := ctx.GitRepo.CommitObject(ctx.Hash) + if err != nil { + http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError) + return + } + + file, err := commit.File(pathValue) + if err != nil { + http.NotFound(w, r) + return + } + + content, err := file.Contents() + if err != nil { + http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError) + return + } + + data := struct { + *RepoContext + Path string + Content template.HTML + }{ + RepoContext: ctx, + Path: pathValue, + Content: highlight(pathValue, content), + } + + err = templates.ExecuteTemplate(w, "blob.html", data) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func rawHandler(w http.ResponseWriter, r *http.Request) { + ctx, err := getRepoContext(w, r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + pathValue := r.PathValue("path") + commit, err := ctx.GitRepo.CommitObject(ctx.Hash) + if err != nil { + http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError) + return + } + + file, err := commit.File(pathValue) + if err != nil { + http.NotFound(w, r) + return + } + + reader, err := file.Reader() + if err != nil { + http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError) + return + } + defer reader.Close() + + w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", pathValue)) + io.Copy(w, reader) +} + +func archiveHandler(w http.ResponseWriter, r *http.Request) { + ctx, err := getRepoContext(w, r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + pathValue := r.PathValue("path") + commit, err := ctx.GitRepo.CommitObject(ctx.Hash) + if err != nil { + http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError) + return + } + + tree, err := commit.Tree() + if err != nil { + http.Error(w, fmt.Sprintf("Error getting tree: %v", err), http.StatusInternalServerError) + return + } + + if pathValue != "" { + tree, err = tree.Tree(pathValue) + if err != nil { + http.NotFound(w, r) + return + } + } + + filename := ctx.Repo.Name + if pathValue != "" { + filename = path.Base(pathValue) + } + filename = fmt.Sprintf("%s-%s.tar.gz", filename, ctx.CurrentRef) + + w.Header().Set("Content-Type", "application/gzip") + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) + + gw := gzip.NewWriter(w) + defer gw.Close() + + tw := tar.NewWriter(gw) + defer tw.Close() + + err = tree.Files().ForEach(func(f *object.File) error { + hdr := &tar.Header{ + Name: strings.TrimPrefix(strings.TrimPrefix(f.Name, pathValue), "/"), + Mode: int64(f.Mode), + Size: f.Size, + } + + if err := tw.WriteHeader(hdr); err != nil { + return err + } + + reader, err := f.Reader() + if err != nil { + return err + } + defer reader.Close() + + _, err = io.Copy(tw, reader) + return err + }) + + if err != nil { + log.Printf("Error creating archive: %v", err) + } +} diff --git a/languages.go b/languages.go index f3c7201606d6f2718c9f2015ae7b9f4fb6956626..66b13a68d1c95036c9eb111438a8044873136ee9 100644 --- a/languages.go +++ b/languages.go @@ -13,402 +13,402 @@ "github.com/go-git/go-git/v5/plumbing/object" ) var languageColors = map[string]string{ - "1C Enterprise": "#814CCC", - "ActionScript": "#882B0F", - "Ada": "#02f88c", - "Agda": "#315665", - "AGS Script": "#B9D9FF", - "Alloy": "#64C800", - "AMPL": "#E6EFBB", - "Ant Build System": "#A9157E", - "ANTLR": "#9DC3FF", - "ApacheConf": "#d12127", - "Apex": "#1797c0", - "API Blueprint": "#2ACCA8", - "APL": "#5A8164", - "AppleScript": "#101F1F", - "Arc": "#aa2afe", - "Arduino": "#bd7911", - "ASP": "#6a40fd", - "AspectJ": "#a957b0", - "Assembly": "#6E4C13", - "ATS": "#1ac620", - "Augeas": "#9CC134", - "AutoHotkey": "#6594b9", - "AutoIt": "#1C3552", - "Awk": "#c30e9b", - "Ballerina": "#FF5000", - "Batchfile": "#C1F12E", - "Befunge": "#2F2530", - "Bicep": "#519aba", - "Bison": "#6A463F", - "BitBake": "#00bce4", - "Blade": "#f7523f", - "BlitzBasic": "#00FFAE", - "BlitzMax": "#cd6400", - "Bluespec": "#12223c", - "Boo": "#d4bec1", - "Brainfuck": "#2F2530", - "Brightscript": "#662D91", - "C": "#555555", - "C#": "#178600", - "C++": "#f34b7d", - "C3": "#2563eb", - "Caddyfile": "#22b638", - "Cairo": "#ff4a48", - "Ceylon": "#dfa535", - "Chapel": "#8dc63f", - "ChucK": "#3f8000", - "Cirru": "#ccccff", - "Clarion": "#db901e", - "Clean": "#3F85AF", - "Click": "#E4E6F3", - "CLIPS": "#00A300", - "Clojure": "#db5855", - "CMake": "#DA3434", - "COBOL": "#1d2021", - "CodeQL": "#140f46", - "CoffeeScript": "#244776", - "ColdFusion": "#ed2cd6", - "Common Lisp": "#3fb68b", - "Component Pascal": "#B0CE4E", - "Crystal": "#000100", - "CSON": "#244776", - "Csound": "#1a1a1a", - "CSS": "#563d7c", - "Cuda": "#3A4E3A", - "Curry": "#531242", - "Cycript": "#000000", - "Cython": "#fedf5b", - "D": "#ba595e", - "Dart": "#00B4AB", - "DataWeave": "#003a52", - "Dhall": "#dfafff", - "Dockerfile": "#384d54", - "Dogescript": "#cca760", - "DTrace": "#000000", - "Dylan": "#6c616e", - "E": "#ccce35", - "eC": "#913960", - "ECL": "#8a1267", - "Eiffel": "#4d6977", - "EJS": "#a91e50", - "Elixir": "#6e4a7e", - "Elm": "#60B5CC", - "Emacs Lisp": "#c065db", - "EmberScript": "#FFF4F3", - "EQ": "#a78649", - "Erlang": "#B83998", - "F#": "#b845fc", - "F*": "#572e30", - "Factor": "#636746", - "Fancy": "#7b9db4", - "Fantom": "#14253c", - "Faust": "#c37240", - "Fennel": "#fff3d7", - "fish": "#4aae47", - "FLUX": "#88ccff", - "Forth": "#341708", - "Fortran": "#4d41b1", - "FreeBASIC": "#141AC9", - "Frege": "#00cafe", - "Futhark": "#5f021f", - "G-code": "#D08CF2", - "Game Maker Language": "#71b417", - "GAML": "#FFC766", - "GAMS": "#f49a22", - "GAP": "#0000cc", - "GDScript": "#355570", - "Genie": "#fb855d", - "Genshi": "#951531", - "Gentoo Ebuild": "#9400ff", - "Gherkin": "#5B2063", - "Gleam": "#ffaff3", - "GLSL": "#5686a5", - "Glyph": "#c1ac7f", - "Gnuplot": "#f0a9f0", - "Go": "#00ADD8", - "Golo": "#88562A", - "Gosu": "#82937f", - "Grace": "#615f8b", - "Gradle": "#02303a", - "GraphQL": "#e10098", - "Groovy": "#4298b8", - "Hack": "#878787", - "Haml": "#ece2a9", - "Handlebars": "#f7931e", - "Harbour": "#0e60e3", - "Haskell": "#5e5086", - "Haxe": "#df7900", - "HCL": "#844FBA", - "HiveQL": "#dce200", - "HolyC": "#ffefaf", - "HTML": "#e34c26", - "Hy": "#7790B2", - "IDL": "#a3522f", - "Idris": "#b30000", - "Ignore List": "#000000", - "IGOR Pro": "#0000cc", - "Imba": "#16cec6", - "Inform 7": "#3d9970", - "INI": "#d1dbe0", - "Inno Setup": "#264b99", - "Io": "#a9188d", - "Ioke": "#078193", - "Isabelle": "#FEFE00", - "J": "#9EEDFF", - "Janet": "#0886a5", - "Java": "#b07219", - "JavaScript": "#f1e05a", - "Jinja": "#a52a22", - "Jison": "#56b3cb", - "Jolie": "#843179", - "JSON": "#292929", - "Jsonnet": "#0064bd", - "Julia": "#a270ba", - "Jupyter Notebook": "#DA5B0B", - "Just": "#384d54", - "Kaitai Struct": "#773b37", - "KCL": "#7ABABF", - "Kotlin": "#A97BFF", - "KRL": "#28430A", - "LabVIEW": "#fede06", - "Lasso": "#999999", - "Latte": "#f2a542", - "Lean": "#3d6117", - "Less": "#1d365d", - "Lex": "#DBCA00", - "LigoLANG": "#0e74ff", - "LilyPond": "#9ccc7c", - "Liquid": "#67b8de", - "LiveScript": "#499886", - "LLVM": "#185619", - "Logtalk": "#295b9a", - "LOLCODE": "#cc9900", - "LookML": "#652B81", - "LSL": "#3d9970", - "Lua": "#000080", - "Luau": "#00A2FF", - "M4": "#000000", - "Macaulay2": "#d8ffff", - "Makefile": "#427819", - "Markdown": "#083fa1", - "Marko": "#42bff2", - "Mask": "#f97732", - "MATLAB": "#e16737", - "Max": "#c4a79c", - "MAXScript": "#00a6a6", - "MDX": "#fcb32c", - "Mercury": "#ff2b2b", - "Meson": "#007800", - "Metal": "#8f14e9", - "MiniYAML": "#ff1111", - "Mint": "#02b046", - "Mirah": "#c7a938", - "Modelica": "#de1d31", - "Modula-2": "#10253f", - "Mojo": "#ff4c1f", - "MoonScript": "#ff4585", - "Move": "#4a137a", - "MQL4": "#62A8D6", - "MQL5": "#4A76B8", - "MTML": "#b7e1f4", - "Mustache": "#724b3b", - "Nemerle": "#3d3c6e", - "nesC": "#94B0C7", - "NetLinx": "#0aa0ff", - "NetLogo": "#ff6375", - "NewLisp": "#87AED7", - "Nextflow": "#3ac486", - "Nginx": "#009639", - "Nim": "#ffc200", - "Nit": "#009917", - "Nix": "#7e7eff", - "Nushell": "#4E9906", - "NWScript": "#111522", - "Objective-C": "#438eff", - "Objective-C++": "#6866fb", - "Objective-J": "#ff0c5a", - "OCaml": "#ef7a08", - "Odin": "#60AFFE", - "Omgrofl": "#cabbff", - "ooc": "#b0b77e", - "Opal": "#f7ede0", - "Open Policy Agent": "#7d9199", - "OpenCL": "#ed2e2d", - "OpenEdge ABL": "#5ce600", - "OpenQASM": "#AA70FF", - "OpenSCAD": "#e5cd45", - "Org": "#77aa99", - "Ox": "#000000", - "Oxygene": "#cdd0e3", - "Oz": "#fab738", - "P4": "#7055b5", - "Papyrus": "#660000", - "Parrot": "#f3ca0a", - "Pascal": "#E3F171", - "Pawn": "#dbb284", - "Pep8": "#C76F5B", - "Perl": "#0298c3", - "PHP": "#4f5d95", - "PicoLisp": "#6067af", - "PigLatin": "#fce7de", - "Pike": "#005390", - "PLpgSQL": "#336790", - "PLSQL": "#dad8d8", - "PogoScript": "#d80073", - "Polar": "#316880", - "Pony": "#000000", - "PostScript": "#da291c", - "PowerShell": "#012456", - "Prisma": "#0c344b", - "Processing": "#0096D8", - "Prolog": "#74283c", - "Promela": "#de3900", - "Protocol Buffer": "#000000", - "Pug": "#a86454", - "Puppet": "#302B6D", - "PureBasic": "#5a6986", - "PureScript": "#1D222D", - "Python": "#3572A5", - "QMake": "#000000", - "QML": "#44a51c", - "Qt Script": "#00b0ff", - "Quake": "#882303", - "R": "#198CE7", - "Racket": "#3c5caa", - "Ragel": "#9d5200", - "Raku": "#0000fb", - "RAML": "#77d9fb", - "Razor": "#512be4", - "Rebol": "#358a5b", - "Red": "#ee0000", - "Redcode": "#000000", - "Ren'Py": "#ff7f7f", - "RenderScript": "#000000", - "Rescript": "#ed4e4e", - "REXX": "#d90e09", - "Ring": "#2D54CB", - "Riot": "#A71E22", - "RMarkdown": "#198ce7", - "RobotFramework": "#00c0b5", - "Roff": "#ecdebe", - "Rouge": "#cc0000", - "Ruby": "#701516", - "RUNOFF": "#660000", - "Rust": "#dea584", - "Sage": "#000000", - "SaltStack": "#646464", - "SAS": "#B34936", - "Sass": "#a53b70", - "Scala": "#c22d40", - "Scaml": "#bd181a", - "Scheme": "#1e4aec", - "Scilab": "#ca0f21", - "SCSS": "#c6538c", - "sed": "#64b970", - "Self": "#0579aa", - "ShaderLab": "#222c37", - "Shell": "#89e051", - "Shen": "#120F14", - "Sieve": "#000000", - "Slash": "#007eff", - "Slice": "#003fa2", - "Slim": "#2b2b2b", - "Smali": "#000000", - "Smalltalk": "#596706", - "Smarty": "#f0c040", - "Smithy": "#c44536", - "SmPL": "#c92223", - "Solidity": "#AA6746", - "SourcePawn": "#f69e1d", - "SPARQL": "#0C4597", - "SQF": "#3F3F3F", - "SQL": "#e38c00", - "SQLPL": "#e38c00", - "Squirrel": "#800000", - "SRecode Template": "#348a34", - "Stan": "#b2011d", - "Standard ML": "#dc566d", - "Starlark": "#76d275", - "Stata": "#1a5f91", - "STL": "#373b3e", - "Stylus": "#ff6347", - "SuperCollider": "#46390b", - "Svelte": "#ff3e00", - "SVG": "#ff9900", - "Swift": "#F05138", - "SWIG": "#000000", - "SystemVerilog": "#DAE1C2", - "Tcl": "#e4cc98", - "Tcsh": "#000000", - "Terra": "#000000", - "TeX": "#3D6117", - "Thrift": "#D88E35", - "TI Program": "#A0AAAD", - "TLA": "#4b0082", - "TOML": "#9c4221", - "TSQL": "#e38c00", - "TSX": "#3178c6", - "Turing": "#cf142b", - "Turtle": "#EEFF11", - "Twig": "#c1d026", - "TXL": "#0178b8", - "TypeScript": "#3178c6", - "Typst": "#239dad", - "Unified Parallel C": "#4e3617", - "Unity3D Asset": "#222c37", - "Uno": "#9933cc", - "UnrealScript": "#a54c4d", - "UrWeb": "#ccc", - "V": "#4f87c4", - "Vala": "#fbe5cd", - "Valve Data Format": "#f26025", - "VBA": "#867db1", - "VBScript": "#15dcdc", - "VCL": "#148AA8", - "Verilog": "#b2b7f8", - "VHDL": "#adb2cb", - "Vim Help File": "#199f4b", - "Vim Script": "#199f4b", - "Visual Basic .NET": "#9400ff", - "Volt": "#1F1F1F", - "Vue": "#41b883", - "Vyper": "#2980b9", - "WDL": "#42f1f4", - "WebAssembly": "#04133b", - "WebIDL": "#000000", - "Whiley": "#d5c397", - "Wikitext": "#fc5757", - "Windows Registry Entries": "#52a5df", - "Witcher Script": "#ff0000", - "Wollok": "#a23738", + "1C Enterprise": "#814CCC", + "ActionScript": "#882B0F", + "Ada": "#02f88c", + "Agda": "#315665", + "AGS Script": "#B9D9FF", + "Alloy": "#64C800", + "AMPL": "#E6EFBB", + "Ant Build System": "#A9157E", + "ANTLR": "#9DC3FF", + "ApacheConf": "#d12127", + "Apex": "#1797c0", + "API Blueprint": "#2ACCA8", + "APL": "#5A8164", + "AppleScript": "#101F1F", + "Arc": "#aa2afe", + "Arduino": "#bd7911", + "ASP": "#6a40fd", + "AspectJ": "#a957b0", + "Assembly": "#6E4C13", + "ATS": "#1ac620", + "Augeas": "#9CC134", + "AutoHotkey": "#6594b9", + "AutoIt": "#1C3552", + "Awk": "#c30e9b", + "Ballerina": "#FF5000", + "Batchfile": "#C1F12E", + "Befunge": "#2F2530", + "Bicep": "#519aba", + "Bison": "#6A463F", + "BitBake": "#00bce4", + "Blade": "#f7523f", + "BlitzBasic": "#00FFAE", + "BlitzMax": "#cd6400", + "Bluespec": "#12223c", + "Boo": "#d4bec1", + "Brainfuck": "#2F2530", + "Brightscript": "#662D91", + "C": "#555555", + "C#": "#178600", + "C++": "#f34b7d", + "C3": "#2563eb", + "Caddyfile": "#22b638", + "Cairo": "#ff4a48", + "Ceylon": "#dfa535", + "Chapel": "#8dc63f", + "ChucK": "#3f8000", + "Cirru": "#ccccff", + "Clarion": "#db901e", + "Clean": "#3F85AF", + "Click": "#E4E6F3", + "CLIPS": "#00A300", + "Clojure": "#db5855", + "CMake": "#DA3434", + "COBOL": "#1d2021", + "CodeQL": "#140f46", + "CoffeeScript": "#244776", + "ColdFusion": "#ed2cd6", + "Common Lisp": "#3fb68b", + "Component Pascal": "#B0CE4E", + "Crystal": "#000100", + "CSON": "#244776", + "Csound": "#1a1a1a", + "CSS": "#563d7c", + "Cuda": "#3A4E3A", + "Curry": "#531242", + "Cycript": "#000000", + "Cython": "#fedf5b", + "D": "#ba595e", + "Dart": "#00B4AB", + "DataWeave": "#003a52", + "Dhall": "#dfafff", + "Dockerfile": "#384d54", + "Dogescript": "#cca760", + "DTrace": "#000000", + "Dylan": "#6c616e", + "E": "#ccce35", + "eC": "#913960", + "ECL": "#8a1267", + "Eiffel": "#4d6977", + "EJS": "#a91e50", + "Elixir": "#6e4a7e", + "Elm": "#60B5CC", + "Emacs Lisp": "#c065db", + "EmberScript": "#FFF4F3", + "EQ": "#a78649", + "Erlang": "#B83998", + "F#": "#b845fc", + "F*": "#572e30", + "Factor": "#636746", + "Fancy": "#7b9db4", + "Fantom": "#14253c", + "Faust": "#c37240", + "Fennel": "#fff3d7", + "fish": "#4aae47", + "FLUX": "#88ccff", + "Forth": "#341708", + "Fortran": "#4d41b1", + "FreeBASIC": "#141AC9", + "Frege": "#00cafe", + "Futhark": "#5f021f", + "G-code": "#D08CF2", + "Game Maker Language": "#71b417", + "GAML": "#FFC766", + "GAMS": "#f49a22", + "GAP": "#0000cc", + "GDScript": "#355570", + "Genie": "#fb855d", + "Genshi": "#951531", + "Gentoo Ebuild": "#9400ff", + "Gherkin": "#5B2063", + "Gleam": "#ffaff3", + "GLSL": "#5686a5", + "Glyph": "#c1ac7f", + "Gnuplot": "#f0a9f0", + "Go": "#00ADD8", + "Golo": "#88562A", + "Gosu": "#82937f", + "Grace": "#615f8b", + "Gradle": "#02303a", + "GraphQL": "#e10098", + "Groovy": "#4298b8", + "Hack": "#878787", + "Haml": "#ece2a9", + "Handlebars": "#f7931e", + "Harbour": "#0e60e3", + "Haskell": "#5e5086", + "Haxe": "#df7900", + "HCL": "#844FBA", + "HiveQL": "#dce200", + "HolyC": "#ffefaf", + "HTML": "#e34c26", + "Hy": "#7790B2", + "IDL": "#a3522f", + "Idris": "#b30000", + "Ignore List": "#000000", + "IGOR Pro": "#0000cc", + "Imba": "#16cec6", + "Inform 7": "#3d9970", + "INI": "#d1dbe0", + "Inno Setup": "#264b99", + "Io": "#a9188d", + "Ioke": "#078193", + "Isabelle": "#FEFE00", + "J": "#9EEDFF", + "Janet": "#0886a5", + "Java": "#b07219", + "JavaScript": "#f1e05a", + "Jinja": "#a52a22", + "Jison": "#56b3cb", + "Jolie": "#843179", + "JSON": "#292929", + "Jsonnet": "#0064bd", + "Julia": "#a270ba", + "Jupyter Notebook": "#DA5B0B", + "Just": "#384d54", + "Kaitai Struct": "#773b37", + "KCL": "#7ABABF", + "Kotlin": "#A97BFF", + "KRL": "#28430A", + "LabVIEW": "#fede06", + "Lasso": "#999999", + "Latte": "#f2a542", + "Lean": "#3d6117", + "Less": "#1d365d", + "Lex": "#DBCA00", + "LigoLANG": "#0e74ff", + "LilyPond": "#9ccc7c", + "Liquid": "#67b8de", + "LiveScript": "#499886", + "LLVM": "#185619", + "Logtalk": "#295b9a", + "LOLCODE": "#cc9900", + "LookML": "#652B81", + "LSL": "#3d9970", + "Lua": "#000080", + "Luau": "#00A2FF", + "M4": "#000000", + "Macaulay2": "#d8ffff", + "Makefile": "#427819", + "Markdown": "#083fa1", + "Marko": "#42bff2", + "Mask": "#f97732", + "MATLAB": "#e16737", + "Max": "#c4a79c", + "MAXScript": "#00a6a6", + "MDX": "#fcb32c", + "Mercury": "#ff2b2b", + "Meson": "#007800", + "Metal": "#8f14e9", + "MiniYAML": "#ff1111", + "Mint": "#02b046", + "Mirah": "#c7a938", + "Modelica": "#de1d31", + "Modula-2": "#10253f", + "Mojo": "#ff4c1f", + "MoonScript": "#ff4585", + "Move": "#4a137a", + "MQL4": "#62A8D6", + "MQL5": "#4A76B8", + "MTML": "#b7e1f4", + "Mustache": "#724b3b", + "Nemerle": "#3d3c6e", + "nesC": "#94B0C7", + "NetLinx": "#0aa0ff", + "NetLogo": "#ff6375", + "NewLisp": "#87AED7", + "Nextflow": "#3ac486", + "Nginx": "#009639", + "Nim": "#ffc200", + "Nit": "#009917", + "Nix": "#7e7eff", + "Nushell": "#4E9906", + "NWScript": "#111522", + "Objective-C": "#438eff", + "Objective-C++": "#6866fb", + "Objective-J": "#ff0c5a", + "OCaml": "#ef7a08", + "Odin": "#60AFFE", + "Omgrofl": "#cabbff", + "ooc": "#b0b77e", + "Opal": "#f7ede0", + "Open Policy Agent": "#7d9199", + "OpenCL": "#ed2e2d", + "OpenEdge ABL": "#5ce600", + "OpenQASM": "#AA70FF", + "OpenSCAD": "#e5cd45", + "Org": "#77aa99", + "Ox": "#000000", + "Oxygene": "#cdd0e3", + "Oz": "#fab738", + "P4": "#7055b5", + "Papyrus": "#660000", + "Parrot": "#f3ca0a", + "Pascal": "#E3F171", + "Pawn": "#dbb284", + "Pep8": "#C76F5B", + "Perl": "#0298c3", + "PHP": "#4f5d95", + "PicoLisp": "#6067af", + "PigLatin": "#fce7de", + "Pike": "#005390", + "PLpgSQL": "#336790", + "PLSQL": "#dad8d8", + "PogoScript": "#d80073", + "Polar": "#316880", + "Pony": "#000000", + "PostScript": "#da291c", + "PowerShell": "#012456", + "Prisma": "#0c344b", + "Processing": "#0096D8", + "Prolog": "#74283c", + "Promela": "#de3900", + "Protocol Buffer": "#000000", + "Pug": "#a86454", + "Puppet": "#302B6D", + "PureBasic": "#5a6986", + "PureScript": "#1D222D", + "Python": "#3572A5", + "QMake": "#000000", + "QML": "#44a51c", + "Qt Script": "#00b0ff", + "Quake": "#882303", + "R": "#198CE7", + "Racket": "#3c5caa", + "Ragel": "#9d5200", + "Raku": "#0000fb", + "RAML": "#77d9fb", + "Razor": "#512be4", + "Rebol": "#358a5b", + "Red": "#ee0000", + "Redcode": "#000000", + "Ren'Py": "#ff7f7f", + "RenderScript": "#000000", + "Rescript": "#ed4e4e", + "REXX": "#d90e09", + "Ring": "#2D54CB", + "Riot": "#A71E22", + "RMarkdown": "#198ce7", + "RobotFramework": "#00c0b5", + "Roff": "#ecdebe", + "Rouge": "#cc0000", + "Ruby": "#701516", + "RUNOFF": "#660000", + "Rust": "#dea584", + "Sage": "#000000", + "SaltStack": "#646464", + "SAS": "#B34936", + "Sass": "#a53b70", + "Scala": "#c22d40", + "Scaml": "#bd181a", + "Scheme": "#1e4aec", + "Scilab": "#ca0f21", + "SCSS": "#c6538c", + "sed": "#64b970", + "Self": "#0579aa", + "ShaderLab": "#222c37", + "Shell": "#89e051", + "Shen": "#120F14", + "Sieve": "#000000", + "Slash": "#007eff", + "Slice": "#003fa2", + "Slim": "#2b2b2b", + "Smali": "#000000", + "Smalltalk": "#596706", + "Smarty": "#f0c040", + "Smithy": "#c44536", + "SmPL": "#c92223", + "Solidity": "#AA6746", + "SourcePawn": "#f69e1d", + "SPARQL": "#0C4597", + "SQF": "#3F3F3F", + "SQL": "#e38c00", + "SQLPL": "#e38c00", + "Squirrel": "#800000", + "SRecode Template": "#348a34", + "Stan": "#b2011d", + "Standard ML": "#dc566d", + "Starlark": "#76d275", + "Stata": "#1a5f91", + "STL": "#373b3e", + "Stylus": "#ff6347", + "SuperCollider": "#46390b", + "Svelte": "#ff3e00", + "SVG": "#ff9900", + "Swift": "#F05138", + "SWIG": "#000000", + "SystemVerilog": "#DAE1C2", + "Tcl": "#e4cc98", + "Tcsh": "#000000", + "Terra": "#000000", + "TeX": "#3D6117", + "Thrift": "#D88E35", + "TI Program": "#A0AAAD", + "TLA": "#4b0082", + "TOML": "#9c4221", + "TSQL": "#e38c00", + "TSX": "#3178c6", + "Turing": "#cf142b", + "Turtle": "#EEFF11", + "Twig": "#c1d026", + "TXL": "#0178b8", + "TypeScript": "#3178c6", + "Typst": "#239dad", + "Unified Parallel C": "#4e3617", + "Unity3D Asset": "#222c37", + "Uno": "#9933cc", + "UnrealScript": "#a54c4d", + "UrWeb": "#ccc", + "V": "#4f87c4", + "Vala": "#fbe5cd", + "Valve Data Format": "#f26025", + "VBA": "#867db1", + "VBScript": "#15dcdc", + "VCL": "#148AA8", + "Verilog": "#b2b7f8", + "VHDL": "#adb2cb", + "Vim Help File": "#199f4b", + "Vim Script": "#199f4b", + "Visual Basic .NET": "#9400ff", + "Volt": "#1F1F1F", + "Vue": "#41b883", + "Vyper": "#2980b9", + "WDL": "#42f1f4", + "WebAssembly": "#04133b", + "WebIDL": "#000000", + "Whiley": "#d5c397", + "Wikitext": "#fc5757", + "Windows Registry Entries": "#52a5df", + "Witcher Script": "#ff0000", + "Wollok": "#a23738", "World of Warcraft Addon Data": "#f7e43a", - "Wren": "#383838", - "X10": "#4B6BEF", - "xBase": "#403a40", - "XC": "#99FF33", - "XML": "#0060ac", - "XML Property List": "#0060ac", - "Xojo": "#81bd41", - "Xonsh": "#285880", - "XOTL": "#000000", - "XQuery": "#5232e7", - "XSLT": "#EB8E35", - "Xtend": "#24255d", - "Yacc": "#4B6C4B", - "YAML": "#cb171e", - "YANG": "#000000", - "YARA": "#220000", - "YASnippet": "#32AB90", - "ZAP": "#0d6616", - "Zeek": "#000000", - "ZenScript": "#00BCD1", - "Zephir": "#118f9e", - "Zig": "#ec915c", - "ZIL": "#dc75e5", - "Zimpl": "#d67711", - "Tree-sitter Query": "#9440ff", + "Wren": "#383838", + "X10": "#4B6BEF", + "xBase": "#403a40", + "XC": "#99FF33", + "XML": "#0060ac", + "XML Property List": "#0060ac", + "Xojo": "#81bd41", + "Xonsh": "#285880", + "XOTL": "#000000", + "XQuery": "#5232e7", + "XSLT": "#EB8E35", + "Xtend": "#24255d", + "Yacc": "#4B6C4B", + "YAML": "#cb171e", + "YANG": "#000000", + "YARA": "#220000", + "YASnippet": "#32AB90", + "ZAP": "#0d6616", + "Zeek": "#000000", + "ZenScript": "#00BCD1", + "Zephir": "#118f9e", + "Zig": "#ec915c", + "ZIL": "#dc75e5", + "Zimpl": "#d67711", + "Tree-sitter Query": "#9440ff", } func getLanguageStats(repoName string, commitHash string, tree *object.Tree) ([]LanguageStat, error) { @@ -432,7 +432,6 @@ filesChan := make(chan *object.File, numWorkers*2) resultsChan := make(chan result, numWorkers*2) var wg sync.WaitGroup - // Workers for i := 0; i < numWorkers; i++ { wg.Add(1) go func() { @@ -475,7 +474,6 @@ } }() } - // Collector languages := make(map[string]int64) var totalSize int64 done := make(chan struct{}) @@ -487,7 +485,6 @@ } close(done) }() - // Producer err := tree.Files().ForEach(func(f *object.File) error { filesChan <- f return nil @@ -525,7 +522,6 @@ stats[i].Offset = currentOffset currentOffset += stats[i].Percentage } - // Store in cache langCache.Store(key, stats) NotifySave() @@ -538,5 +534,5 @@ func getLanguageColor(lang string) string { if color, ok := languageColors[lang]; ok { return color } - return "#8b8b8b" // Default gray + return "#8b8b8b" } diff --git a/mdalerts.go b/mdalerts.go index 5cf1869f1738c7080f0bffecac8ee2bfe6904747..4a200a9af21627b545506a64b8ff2f57ba3d270f 100644 --- a/mdalerts.go +++ b/mdalerts.go @@ -65,7 +65,7 @@ type alertTransformer struct{} func (a *alertTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { source := reader.Source() - + type matchInfo struct { container ast.Node para *ast.Paragraph @@ -99,7 +99,6 @@ if p == nil { return ast.WalkContinue, nil } - // Check paragraph text directly using p.Text(source) pText := p.Text(source) raw := string(pText) trimmed := strings.TrimLeft(raw, " \t\n\r") diff --git a/models.go b/models.go index 4472b7d1044fbd93e13304a4c46b58e0d6531395..8c15a6cd2847cc07acf8d24d370c375b3dae1c82 100644 --- a/models.go +++ b/models.go @@ -1,8 +1,8 @@ package main import ( - "time" "encoding/xml" + "time" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" diff --git a/static/style.css b/static/style.css index 16d078651e81639d411d4e2fb26844e79b0b48e6..9beb36a43377d6ee03d6f0e00ce587bb63393dcc 100644 --- a/static/style.css +++ b/static/style.css @@ -254,10 +254,12 @@ pre { padding: 0.5em; } .markdown-alert { padding: 0.75rem 1rem; - margin-bottom: 1rem; color: inherit; border: 1px solid; border-left: 0.25em solid; + + margin-block-start: 1em; + margin-block-end: 1em; .markdown-alert-title { display: flex;