Hi… I am well aware that this diff view is very suboptimal. It will be fixed when the refactored server comes along!
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 -}}