Lindenii Project Forge
Cache commit logs on the repo index page
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileContributor: Runxi Yu <https://runxiyu.org> package main import ( "html/template" "github.com/dgraph-io/ristretto/v2" "go.lindenii.runxiyu.org/lindenii-common/clog" ) type treeReadmeCacheEntry struct { DisplayTree []displayTreeEntry ReadmeFilename string ReadmeRendered template.HTML } var treeReadmeCache *ristretto.Cache[[]byte, treeReadmeCacheEntry] func init() { var err error treeReadmeCache, err = ristretto.NewCache(&ristretto.Config[[]byte, treeReadmeCacheEntry]{ NumCounters: 1e4, MaxCost: 1 << 30, BufferItems: 64, }) if err != nil { clog.Fatal(1, "Error initializing indexPageCache: "+err.Error()) } }
var indexCommitsDisplayCache *ristretto.Cache[[]byte, []commitDisplay] func init() { var err error indexCommitsDisplayCache, err = ristretto.NewCache(&ristretto.Config[[]byte, []commitDisplay]{ NumCounters: 1e4, MaxCost: 1 << 30, BufferItems: 64, }) if err != nil { clog.Fatal(1, "Error initializing indexCommitsCache: "+err.Error()) } }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileContributor: Runxi Yu <https://runxiyu.org> package main import ( "context" "errors" "io" "iter" "os" "strings" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/jackc/pgx/v5/pgtype" ) // openRepo opens a git repository by group and repo name. func openRepo(ctx context.Context, groupPath []string, repoName string) (repo *git.Repository, description string, repoID int, err error) { var fsPath string err = database.QueryRow(ctx, ` WITH RECURSIVE group_path_cte AS ( -- Start: match the first name in the path where parent_group IS NULL SELECT id, parent_group, name, 1 AS depth FROM groups WHERE name = ($1::text[])[1] AND parent_group IS NULL UNION ALL -- Recurse: join next segment of the path SELECT g.id, g.parent_group, g.name, group_path_cte.depth + 1 FROM groups g JOIN group_path_cte ON g.parent_group = group_path_cte.id WHERE g.name = ($1::text[])[group_path_cte.depth + 1] AND group_path_cte.depth + 1 <= cardinality($1::text[]) ) SELECT r.filesystem_path, COALESCE(r.description, ''), r.id FROM group_path_cte g JOIN repos r ON r.group_id = g.id WHERE g.depth = cardinality($1::text[]) AND r.name = $2 `, pgtype.FlatArray[string](groupPath), repoName).Scan(&fsPath, &description, &repoID) if err != nil { return } repo, err = git.PlainOpen(fsPath) return } // go-git's tree entries are not friendly for use in HTML templates. type displayTreeEntry struct { Name string Mode string Size int64 IsFile bool IsSubtree bool } func makeDisplayTree(tree *object.Tree) (displayTree []displayTreeEntry) { for _, entry := range tree.Entries { displayEntry := displayTreeEntry{} //exhaustruct:ignore var err error var osMode os.FileMode if osMode, err = entry.Mode.ToOSFileMode(); err != nil { displayEntry.Mode = "x---------" } else { displayEntry.Mode = osMode.String() } displayEntry.IsFile = entry.Mode.IsFile() if displayEntry.Size, err = tree.Size(entry.Name); err != nil { displayEntry.Size = 0 } displayEntry.Name = strings.TrimPrefix(entry.Name, "/") displayTree = append(displayTree, displayEntry) } return displayTree } func commitIterSeqErr(commitIter object.CommitIter) (iter.Seq[*object.Commit], *error) { var err error return func(yield func(*object.Commit) bool) { for { commit, err2 := commitIter.Next() if err2 != nil { if errors.Is(err2, io.EOF) { return } err = err2 return } if !yield(commit) { return } } }, &err } func iterSeqLimit[T any](s iter.Seq[T], n uint) iter.Seq[T] { return func(yield func(T) bool) { var iterations uint for v := range s { if iterations > n-1 { return } if !yield(v) { return } iterations++ } } } func getRecentCommits(repo *git.Repository, headHash plumbing.Hash, numCommits int) (recentCommits []*object.Commit, err error) { var commitIter object.CommitIter var thisCommit *object.Commit commitIter, err = repo.Log(&git.LogOptions{From: headHash}) //exhaustruct:ignore if err != nil { return nil, err } recentCommits = make([]*object.Commit, 0) defer commitIter.Close() if numCommits < 0 { for { thisCommit, err = commitIter.Next() if errors.Is(err, io.EOF) { return recentCommits, nil } else if err != nil { return nil, err } recentCommits = append(recentCommits, thisCommit) } } else { for range numCommits { thisCommit, err = commitIter.Next() if errors.Is(err, io.EOF) { return recentCommits, nil } else if err != nil { return nil, err } recentCommits = append(recentCommits, thisCommit) } } return recentCommits, err }
func getRecentCommitsDisplay(repo *git.Repository, headHash plumbing.Hash, numCommits int) (recentCommits []commitDisplay, err error) { var commitIter object.CommitIter var thisCommit *object.Commit commitIter, err = repo.Log(&git.LogOptions{From: headHash}) //exhaustruct:ignore if err != nil { return nil, err } recentCommits = make([]commitDisplay, 0) defer commitIter.Close() if numCommits < 0 { for { thisCommit, err = commitIter.Next() if errors.Is(err, io.EOF) { return recentCommits, nil } else if err != nil { return nil, err } recentCommits = append(recentCommits, commitDisplay{ thisCommit.Hash, thisCommit.Author, thisCommit.Committer, thisCommit.Message, thisCommit.TreeHash, }) } } else { for range numCommits { thisCommit, err = commitIter.Next() if errors.Is(err, io.EOF) { return recentCommits, nil } else if err != nil { return nil, err } recentCommits = append(recentCommits, commitDisplay{ thisCommit.Hash, thisCommit.Author, thisCommit.Committer, thisCommit.Message, thisCommit.TreeHash, }) } } return recentCommits, err } type commitDisplay struct { Hash plumbing.Hash Author object.Signature Committer object.Signature Message string TreeHash plumbing.Hash }
func fmtCommitAsPatch(commit *object.Commit) (parentCommitHash plumbing.Hash, patch *object.Patch, err error) { var parentCommit *object.Commit var commitTree *object.Tree parentCommit, err = commit.Parent(0) switch { case errors.Is(err, object.ErrParentNotFound): if commitTree, err = commit.Tree(); err != nil { return } if patch, err = nullTree.Patch(commitTree); err != nil { return } case err != nil: return default: parentCommitHash = parentCommit.Hash if patch, err = parentCommit.Patch(commit); err != nil { return } } return } var nullTree object.Tree
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileContributor: Runxi Yu <https://runxiyu.org> package main import (
"iter"
"net/http" "strings" "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" "github.com/go-git/go-git/v5/plumbing/storer" ) func httpHandleRepoIndex(writer http.ResponseWriter, _ *http.Request, params map[string]any) { var repo *git.Repository var repoName string var groupPath []string var refHash plumbing.Hash var refHashSlice []byte var err error
var logOptions git.LogOptions var commitIter object.CommitIter var commitIterSeq iter.Seq[*object.Commit]
var commitObj *object.Commit var tree *object.Tree var notes []string var branches []string var branchesIter storer.ReferenceIter
var commits []commitDisplay
repo, repoName, groupPath = params["repo"].(*git.Repository), params["repo_name"].(string), params["group_path"].([]string) if strings.Contains(repoName, "\n") || sliceContainsNewlines(groupPath) { notes = append(notes, "Path contains newlines; HTTP Git access impossible") } refHash, err = getRefHash(repo, params["ref_type"].(string), params["ref_name"].(string)) if err != nil { goto no_ref } refHashSlice = refHash[:] branchesIter, err = repo.Branches() if err == nil { _ = branchesIter.ForEach(func(branch *plumbing.Reference) error { branches = append(branches, branch.Name().Short()) return nil }) } params["branches"] = branches
// TODO: Cache logOptions = git.LogOptions{From: refHash} //exhaustruct:ignore if commitIter, err = repo.Log(&logOptions); err != nil { goto no_ref
if value, found := indexCommitsDisplayCache.Get(refHashSlice); found { if value != nil { commits = value } else { goto readme } } else { start := time.Now() commits, err = getRecentCommitsDisplay(repo, refHash, 5) if err != nil { commits = nil } cost := time.Since(start).Nanoseconds() indexCommitsDisplayCache.Set(refHashSlice, commits, cost) if err != nil { goto readme }
}
commitIterSeq, params["commits_err"] = commitIterSeqErr(commitIter) params["commits"] = iterSeqLimit(commitIterSeq, 3)
params["commits"] = commits readme:
if value, found := treeReadmeCache.Get(refHashSlice); found { params["files"] = value.DisplayTree params["readme_filename"] = value.ReadmeFilename params["readme"] = value.ReadmeRendered } else { start := time.Now() if commitObj, err = repo.CommitObject(refHash); err != nil { goto no_ref } if tree, err = commitObj.Tree(); err != nil { goto no_ref } displayTree := makeDisplayTree(tree) readmeFilename, readmeRendered := renderReadmeAtTree(tree) cost := time.Since(start).Nanoseconds() params["files"] = displayTree params["readme_filename"] = readmeFilename params["readme"] = readmeRendered entry := treeReadmeCacheEntry{ DisplayTree: displayTree, ReadmeFilename: readmeFilename, ReadmeRendered: readmeRendered, } treeReadmeCache.Set(refHashSlice, entry, cost) } no_ref: params["http_clone_url"] = genHTTPRemoteURL(groupPath, repoName) params["ssh_clone_url"] = genSSHRemoteURL(groupPath, repoName) params["notes"] = notes renderTemplate(writer, "repo_index", params) }
{{/* SPDX-License-Identifier: AGPL-3.0-only SPDX-FileContributor: Runxi Yu <https://runxiyu.org> */}} {{- define "repo_contrib_index" -}} <!DOCTYPE html> <html lang="en"> <head> {{- template "head_common" . -}} <title>Merge requests – {{ .repo_name }} – {{ template "group_path_plain" .group_path }} – {{ .global.forge_title -}}</title> </head> <body class="repo-contrib-index"> {{- template "header" . -}} <div class="padding-wrapper"> <table id="recent-merge_requests" class="wide rounded"> <thead> <tr class="title-row"> <th colspan="3">Merge requests</th> </tr> </thead> <tr> <th scope="col">Name</th> <th scope="col">Description</th> <th scope="col">Status</th> </tr> <tbody> {{- range .merge_requests -}} <tr>
<td class="merge_request-id">{{- .ID -}}</td> <td class="merge_request-title"><a href="{{- .ID -}}/">{{- .Title -}}</a></td>
<td class="merge_request-id">{{- .Hash -}}</td> <td class="merge_request-title"><a href="{{- .Hash -}}/">{{- .Title -}}</a></td>
<td class="merge_request-status">{{- .Status -}}</td> </tr> {{- end -}} </tbody> </table> </div> <footer> {{- template "footer" . -}} </footer> </body> </html> {{- end -}}
{{/* SPDX-License-Identifier: AGPL-3.0-only SPDX-FileContributor: Runxi Yu <https://runxiyu.org> */}} {{- define "repo_contrib_one" -}} <!DOCTYPE html> <html lang="en"> <head> {{- template "head_common" . -}} <title>Merge requests – {{ .repo_name }} – {{ template "group_path_plain" .group_path }} – {{ .global.forge_title -}}</title> </head> <body class="repo-contrib-one"> {{- template "header" . -}} <div class="padding-wrapper"> <table id="mr-info-table" class="rounded"> <thead> <tr class="title-row"> <th colspan="2">Merge request info</th> </tr> </thead> <tbody> <tr> <th scope="row">ID</th> <td>{{- .mr_id -}}</td> </tr> <tr> <th scope="row">Status</th> <td>{{- .mr_status -}}</td> </tr> <tr> <th scope="row">Title</th> <td>{{- .mr_title -}}</td> </tr> <tr> <th scope="row">Source ref</th> <td>{{- .mr_source_ref -}}</td> </tr> <tr> <th scope="row">Destination branch</th> <td>{{- .mr_destination_branch -}}</td> </tr> <tr> <th scope="row">Merge base</th>
<td>{{- .merge_base.ID.String -}}</td>
<td>{{- .merge_base.Hash.String -}}</td>
</tr> </tbody> </table> </div> <div class="padding-wrapper"> {{- $merge_base := .merge_base -}} {{- $source_commit := .source_commit -}} {{- range .file_patches -}} <div class="file-patch toggle-on-wrapper"> <input type="checkbox" id="toggle-{{- .From.Hash -}}{{- .To.Hash -}}" class="file-toggle toggle-on-toggle"> <label for="toggle-{{- .From.Hash -}}{{- .To.Hash -}}" class="file-header toggle-on-header"> <div> {{- if eq .From.Path "" -}} --- /dev/null {{- else -}} --- a/<a href="../../tree/{{- .From.Path -}}?commit={{- $merge_base.Hash -}}">{{- .From.Path -}}</a> {{ .From.Mode -}} {{- end -}} <br /> {{- if eq .To.Path "" -}} +++ /dev/null {{- else -}} +++ b/<a href="../../tree/{{- .To.Path -}}?commit={{- $source_commit.Hash -}}">{{- .To.Path -}}</a> {{ .To.Mode -}} {{- end -}} </div> </label> <div class="file-content toggle-on-content scroll"> {{- range .Chunks -}} {{- if eq .Operation 0 -}} <pre class="chunk chunk-unchanged">{{ .Content }}</pre> {{- else if eq .Operation 1 -}} <pre class="chunk chunk-addition">{{ .Content }}</pre> {{- else if eq .Operation 2 -}} <pre class="chunk chunk-deletion">{{ .Content }}</pre> {{- else -}} <pre class="chunk chunk-unknown">{{ .Content }}</pre> {{- end -}} {{- end -}} </div> </div> {{- end -}} </div> <footer> {{- template "footer" . -}} </footer> </body> </html> {{- end -}}
{{/* SPDX-License-Identifier: AGPL-3.0-only SPDX-FileContributor: Runxi Yu <https://runxiyu.org> */}} {{- define "repo_index" -}} <!DOCTYPE html> <html lang="en"> <head> {{- template "head_common" . -}} <title>{{ .repo_name }} – {{ template "group_path_plain" .group_path }} – {{ .global.forge_title -}}</title> </head> <body class="repo-index"> {{- template "header" . -}} <div class="padding-wrapper"> <table id="repo-info-table" class="rounded"> <thead> <tr class="title-row"> <th colspan="2">Repo info</th> </tr> </thead> <tbody> <tr> <th scope="row">Name</th> <td>{{- .repo_name -}}</td> </tr> {{- if .repo_description -}} <tr> <th scope="row">Description</th> <td>{{- .repo_description -}}</td> </tr> {{- end -}} <tr> <th scope="row">SSH remote</th> <td><code>{{- .ssh_clone_url -}}</code></td> </tr> {{- if .notes -}} <tr> <th scope="row">Notes</th> <td><ul>{{- range .notes -}}<li>{{- . -}}</li>{{- end -}}</ul></td> </tr> {{- end -}} </tbody> </table> </div> <div class="padding-wrapper"> <table id="branches" class="rounded"> <thead> <tr class="title-row"> <th colspan="1">Branches</th> </tr> </thead> <tbody> {{ range .branches }} <tr> <td> <a href="./?branch={{ . }}">{{ . }}</a> </td> </tr> {{ end }} </tbody> </table> </div> <div class="padding-wrapper"> <p> <a href="contrib/" class="btn-normal">Merge requests</a> </p> </div> {{- if .commits -}} <div class="padding-wrapper scroll"> <table id="recent-commits" class="wide rounded"> <thead> <tr class="title-row"> <th colspan="3">Recent commits (<a href="log/{{- if .ref_type -}}?{{- .ref_type -}}={{- .ref_name -}}{{- end -}}">see all</a>)</th> </tr> <tr> <th scope="col">Title</th> <th scope="col">Author</th> <th scope="col">Author Date</th> </tr> </thead> <tbody> {{- range .commits -}} <tr>
<td class="commit-title"><a href="commit/{{- .ID -}}">{{- .Message | first_line -}}</a></td>
<td class="commit-title"><a href="commit/{{- .Hash -}}">{{- .Message | first_line -}}</a></td>
<td class="commit-author"> <a class="email-name" href="mailto:{{- .Author.Email -}}">{{- .Author.Name -}}</a> </td> <td class="commit-time"> {{- .Author.When.Format "2006-01-02 15:04:05 -0700" -}} </td> </tr> {{- end -}} {{- if dereference_error .commits_err -}} Error while obtaining commit log: {{ .commits_err }} {{- end -}} </tbody> </table> </div> {{- end -}} {{- if .files -}} <div class="padding-wrapper scroll"> <table id="file-tree" class="wide rounded"> <thead> <tr class="title-row"> <th colspan="3">/{{- if .ref_name }} on {{ .ref_name -}}{{- end -}}</th> </tr> <tr> <th scope="col">Mode</th> <th scope="col">Filename</th> <th scope="col">Size</th> </tr> </thead> <tbody> {{- $ref_type := .ref_type -}} {{- $ref := .ref_name -}} {{- range .files -}} <tr> <td class="file-mode">{{- .Mode -}}</td> <td class="file-name"><a href="tree/{{- .Name -}}{{- if not .IsFile -}}/{{- end -}}{{- if $ref_type -}}?{{- $ref_type -}}={{- $ref -}}{{- end -}}">{{- .Name -}}</a>{{- if not .IsFile -}}/{{- end -}}</td> <td class="file-size">{{- .Size -}}</td> </tr> {{- end -}} </tbody> </table> </div> {{- end -}} {{- if .readme -}} <div class="padding-wrapper" id="readme"> {{- .readme -}} </div> {{- end -}} <footer> {{- template "footer" . -}} </footer> </body> </html> {{- end -}}
{{/* SPDX-License-Identifier: AGPL-3.0-only SPDX-FileContributor: Runxi Yu <https://runxiyu.org> */}} {{- define "repo_log" -}} <!DOCTYPE html> <html lang="en"> <head> {{- template "head_common" . -}} <title>Log – {{ .repo_name }} – {{ template "group_path_plain" .group_path }} – {{ .global.forge_title -}}</title> </head> <body class="repo-log"> {{- template "header" . -}} <div class="scroll"> <table id="commits" class="wide rounded"> <thead> <tr class="title-row"> <th colspan="4">Commits {{ if .ref_name }} on {{ .ref_name }}{{ end -}}</th> </tr> <tr> <th scope="col">ID</th> <th scope="col">Title</th> <th scope="col">Author</th> <th scope="col">Time</th> </tr> </thead> <tbody> {{- range .commits -}} <tr>
<td class="commit-id"><a href="../commit/{{- .ID -}}">{{- .ID -}}</a></td>
<td class="commit-id"><a href="../commit/{{- .Hash -}}">{{- .Hash -}}</a></td>
<td class="commit-title">{{- .Message | first_line -}}</td> <td class="commit-author"> <a class="email-name" href="mailto:{{- .Author.Email -}}">{{- .Author.Name -}}</a> </td> <td class="commit-time"> {{- .Author.When.Format "2006-01-02 15:04:05 -0700" -}} </td> </tr> {{- end -}} {{- if dereference_error .commits_err -}} Error while obtaining commit log: {{ .commits_err }} {{- end -}} </tbody> </table> </div> <footer> {{- template "footer" . -}} </footer> </body> </html> {{- end -}}