Hi… I am well aware that this diff view is very suboptimal. It will be fixed when the refactored server comes along!
Be a bit more careful handling size integer overflows and such
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 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.
//
// TODO: This should be deprecated in favor of doing it in the relevant
// request/router context in the future, as it cannot cover the nuance of
// fields needed.
func openRepo(ctx context.Context, groupPath []string, repoName string) (repo *git.Repository, description string, repoID int, fsPath string, err error) {
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.
// This struct is a wrapper that is friendlier for use in templating.
type displayTreeEntry struct {
Name string
Mode string
Size int64
Size uint64
IsFile bool
IsSubtree bool
}
// makeDisplayTree takes git trees of form [object.Tree] and creates a slice of
// [displayTreeEntry] for easier templating.
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
}
size, _ := tree.Size(entry.Name) displayEntry.Size = uint64(size) //#nosec G115
displayEntry.Name = strings.TrimPrefix(entry.Name, "/")
displayTree = append(displayTree, displayEntry)
}
return displayTree
}
// commitIterSeqErr creates an [iter.Seq[*object.Commit]] from an
// [object.CommitIter], and additionally returns a pointer to error.
// The pointer to error is guaranteed to be populated with either nil or the
// error returned by the commit iterator after the returned iterator is
// finished.
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
}
// getRecentCommits fetches numCommits commits, starting from the headHash in a
// repo.
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
}
// getRecentCommitsDisplay generates a slice of [commitDisplay] friendly for
// use in HTML templates, consisting of numCommits commits from headhash in the
// repo.
func getRecentCommitsDisplay(repo *git.Repository, headHash plumbing.Hash, numCommits int) (recentCommits []commitDisplayOld, 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([]commitDisplayOld, 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, commitDisplayOld{
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, commitDisplayOld{
thisCommit.Hash,
thisCommit.Author,
thisCommit.Committer,
thisCommit.Message,
thisCommit.TreeHash,
})
}
}
return recentCommits, err
}
type commitDisplayOld struct {
Hash plumbing.Hash
Author object.Signature
Committer object.Signature
Message string
TreeHash plumbing.Hash
}
// commitToPatch creates an [object.Patch] from the first parent of a given
// [object.Commit].
//
// TODO: This function should be deprecated as it only diffs with the first
// parent and does not correctly handle merge commits.
func commitToPatch(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-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
package main
import (
"errors"
"fmt"
"html/template"
"io"
"net"
"net/http"
"strings"
"git.sr.ht/~sircmpwn/go-bare"
)
// httpHandleRepoRaw serves raw files, or directory listings that point to raw
// files.
func httpHandleRepoRaw(writer http.ResponseWriter, request *http.Request, params map[string]any) {
repoName := params["repo_name"].(string)
groupPath := params["group_path"].([]string)
rawPathSpec := params["rest"].(string)
pathSpec := strings.TrimSuffix(rawPathSpec, "/")
params["path_spec"] = pathSpec
_, repoPath, _, _, _, _, _ := getRepoInfo(request.Context(), groupPath, repoName, "")
conn, err := net.Dial("unix", config.Git.Socket)
if err != nil {
errorPage500(writer, params, "git2d connection failed: "+err.Error())
return
}
defer conn.Close()
brWriter := bare.NewWriter(conn)
brReader := bare.NewReader(conn)
if err := brWriter.WriteData([]byte(repoPath)); err != nil {
errorPage500(writer, params, "sending repo path failed: "+err.Error())
return
}
if err := brWriter.WriteUint(2); err != nil {
errorPage500(writer, params, "sending command failed: "+err.Error())
return
}
if err := brWriter.WriteData([]byte(pathSpec)); err != nil {
errorPage500(writer, params, "sending path failed: "+err.Error())
return
}
status, err := brReader.ReadUint()
if err != nil {
errorPage500(writer, params, "reading status failed: "+err.Error())
return
}
switch status {
case 0:
kind, err := brReader.ReadUint()
if err != nil {
errorPage500(writer, params, "reading object kind failed: "+err.Error())
return
}
switch kind {
case 1:
// Tree
if redirectDir(writer, request) {
return
}
count, err := brReader.ReadUint()
if err != nil {
errorPage500(writer, params, "reading entry count failed: "+err.Error())
return
}
files := make([]displayTreeEntry, 0, count)
for range count {
typeCode, err := brReader.ReadUint()
if err != nil {
errorPage500(writer, params, "error reading entry type: "+err.Error())
return
}
mode, err := brReader.ReadUint()
if err != nil {
errorPage500(writer, params, "error reading entry mode: "+err.Error())
return
}
size, err := brReader.ReadUint()
if err != nil {
errorPage500(writer, params, "error reading entry size: "+err.Error())
return
}
name, err := brReader.ReadData()
if err != nil {
errorPage500(writer, params, "error reading entry name: "+err.Error())
return
}
files = append(files, displayTreeEntry{
Name: string(name),
Mode: fmt.Sprintf("%06o", mode),
Size: int64(size),
Size: size,
IsFile: typeCode == 2,
IsSubtree: typeCode == 1,
})
}
params["files"] = files
params["readme_filename"] = "README.md"
params["readme"] = template.HTML("<p>README rendering here is WIP again</p>") // TODO
renderTemplate(writer, "repo_raw_dir", params)
case 2:
// Blob
if redirectNoDir(writer, request) {
return
}
content, err := brReader.ReadData()
if err != nil && !errors.Is(err, io.EOF) {
errorPage500(writer, params, "error reading blob content: "+err.Error())
return
}
writer.Header().Set("Content-Type", "application/octet-stream")
fmt.Fprint(writer, string(content))
default:
errorPage500(writer, params, fmt.Sprintf("unknown object kind: %d", kind))
}
case 3:
errorPage500(writer, params, "path not found: "+pathSpec)
default:
errorPage500(writer, params, fmt.Sprintf("unknown status code: %d", status))
}
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
package main
import (
"errors"
"fmt"
"html/template"
"io"
"net"
"net/http"
"strings"
"git.sr.ht/~sircmpwn/go-bare"
)
// httpHandleRepoTree provides a friendly, syntax-highlighted view of
// individual files, and provides directory views that link to these files.
//
// TODO: Do not highlight files that are too large.
func httpHandleRepoTree(writer http.ResponseWriter, request *http.Request, params map[string]any) {
repoName := params["repo_name"].(string)
groupPath := params["group_path"].([]string)
rawPathSpec := params["rest"].(string)
pathSpec := strings.TrimSuffix(rawPathSpec, "/")
params["path_spec"] = pathSpec
_, repoPath, _, _, _, _, _ := getRepoInfo(request.Context(), groupPath, repoName, "")
conn, err := net.Dial("unix", config.Git.Socket)
if err != nil {
errorPage500(writer, params, "git2d connection failed: "+err.Error())
return
}
defer conn.Close()
brWriter := bare.NewWriter(conn)
brReader := bare.NewReader(conn)
if err := brWriter.WriteData([]byte(repoPath)); err != nil {
errorPage500(writer, params, "sending repo path failed: "+err.Error())
return
}
if err := brWriter.WriteUint(2); err != nil {
errorPage500(writer, params, "sending command failed: "+err.Error())
return
}
if err := brWriter.WriteData([]byte(pathSpec)); err != nil {
errorPage500(writer, params, "sending path failed: "+err.Error())
return
}
status, err := brReader.ReadUint()
if err != nil {
errorPage500(writer, params, "reading status failed: "+err.Error())
return
}
switch status {
case 0:
kind, err := brReader.ReadUint()
if err != nil {
errorPage500(writer, params, "reading object kind failed: "+err.Error())
return
}
switch kind {
case 1:
// Tree
count, err := brReader.ReadUint()
if err != nil {
errorPage500(writer, params, "reading entry count failed: "+err.Error())
return
}
files := make([]displayTreeEntry, 0, count)
for range count {
typeCode, err := brReader.ReadUint()
if err != nil {
errorPage500(writer, params, "error reading entry type: "+err.Error())
return
}
mode, err := brReader.ReadUint()
if err != nil {
errorPage500(writer, params, "error reading entry mode: "+err.Error())
return
}
size, err := brReader.ReadUint()
if err != nil {
errorPage500(writer, params, "error reading entry size: "+err.Error())
return
}
name, err := brReader.ReadData()
if err != nil {
errorPage500(writer, params, "error reading entry name: "+err.Error())
return
}
files = append(files, displayTreeEntry{
Name: string(name),
Mode: fmt.Sprintf("%06o", mode),
Size: int64(size),
Size: size,
IsFile: typeCode == 2,
IsSubtree: typeCode == 1,
})
}
params["files"] = files
params["readme_filename"] = "README.md"
params["readme"] = template.HTML("<p>README rendering here is WIP again</p>") // TODO
renderTemplate(writer, "repo_tree_dir", params)
case 2:
// Blob
content, err := brReader.ReadData()
if err != nil && !errors.Is(err, io.EOF) {
errorPage500(writer, params, "error reading file content: "+err.Error())
return
}
rendered := renderHighlightedFile(pathSpec, string(content))
params["file_contents"] = rendered
renderTemplate(writer, "repo_tree_file", params)
default:
errorPage500(writer, params, fmt.Sprintf("unknown kind: %d", kind))
return
}
case 3:
errorPage500(writer, params, "path not found: "+pathSpec)
return
default:
errorPage500(writer, params, fmt.Sprintf("unknown status code: %d", status))
}
}