Hi… I am well aware that this diff view is very suboptimal. It will be fixed when the refactored server comes along!
Reduce unnecessary allocations when converting []byte to string
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
package main
import (
"bytes"
"context"
"encoding/hex"
"errors"
"os"
"os/exec"
"path"
"sort"
"strings"
)
func writeTree(ctx context.Context, repoPath string, entries []treeEntry) (string, error) {
var buf bytes.Buffer
sort.Slice(entries, func(i, j int) bool {
nameI, nameJ := entries[i].name, entries[j].name
if nameI == nameJ { // meh
return !(entries[i].mode == "40000") && (entries[j].mode == "40000")
}
if strings.HasPrefix(nameJ, nameI) && len(nameI) < len(nameJ) {
return !(entries[i].mode == "40000")
}
if strings.HasPrefix(nameI, nameJ) && len(nameJ) < len(nameI) {
return entries[j].mode == "40000"
}
return nameI < nameJ
})
for _, e := range entries {
buf.WriteString(e.mode)
buf.WriteByte(' ')
buf.WriteString(e.name)
buf.WriteByte(0)
buf.Write(e.sha)
}
cmd := exec.CommandContext(ctx, "git", "hash-object", "-w", "-t", "tree", "--stdin")
cmd.Env = append(os.Environ(), "GIT_DIR="+repoPath)
cmd.Stdin = &buf
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
return "", err
}
return strings.TrimSpace(out.String()), nil
}
func buildTreeRecursive(ctx context.Context, repoPath, baseTree string, updates map[string][]byte) (string, error) {
treeCache := make(map[string][]treeEntry)
var walk func(string, string) error
walk = func(prefix, sha string) error {
cmd := exec.CommandContext(ctx, "git", "cat-file", "tree", sha)
cmd.Env = append(os.Environ(), "GIT_DIR="+repoPath)
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
return err
}
data := out.Bytes()
i := 0
var entries []treeEntry
for i < len(data) {
modeEnd := bytes.IndexByte(data[i:], ' ')
if modeEnd < 0 {
return errors.New("invalid tree format")
}
mode := string(data[i : i+modeEnd])
mode := bytesToString(data[i : i+modeEnd])
i += modeEnd + 1
nameEnd := bytes.IndexByte(data[i:], 0)
if nameEnd < 0 {
return errors.New("missing null after filename")
}
name := string(data[i : i+nameEnd])
name := bytesToString(data[i : i+nameEnd])
i += nameEnd + 1
if i+20 > len(data) {
return errors.New("unexpected EOF in SHA")
}
shaBytes := data[i : i+20]
i += 20
entries = append(entries, treeEntry{
mode: mode,
name: name,
sha: shaBytes,
})
if mode == "40000" {
subPrefix := path.Join(prefix, name)
if err := walk(subPrefix, hex.EncodeToString(shaBytes)); err != nil {
return err
}
}
}
treeCache[prefix] = entries
return nil
}
if err := walk("", baseTree); err != nil {
return "", err
}
for filePath, blobSha := range updates {
parts := strings.Split(filePath, "/")
dir := strings.Join(parts[:len(parts)-1], "/")
name := parts[len(parts)-1]
entries := treeCache[dir]
found := false
for i, e := range entries {
if e.name == name {
if blobSha == nil {
// Remove TODO
entries = append(entries[:i], entries[i+1:]...)
} else {
entries[i].sha = blobSha
}
found = true
break
}
}
if !found && blobSha != nil {
entries = append(entries, treeEntry{
mode: "100644",
name: name,
sha: blobSha,
})
}
treeCache[dir] = entries
}
built := make(map[string][]byte)
var build func(string) ([]byte, error)
build = func(prefix string) ([]byte, error) {
entries := treeCache[prefix]
for i, e := range entries {
if e.mode == "40000" {
subPrefix := path.Join(prefix, e.name)
if sha, ok := built[subPrefix]; ok {
entries[i].sha = sha
continue
}
newShaStr, err := build(subPrefix)
if err != nil {
return nil, err
}
entries[i].sha = newShaStr
}
}
shaStr, err := writeTree(ctx, repoPath, entries)
if err != nil {
return nil, err
}
shaBytes, err := hex.DecodeString(shaStr)
if err != nil {
return nil, err
}
built[prefix] = shaBytes
return shaBytes, nil
}
rootShaBytes, err := build("")
if err != nil {
return "", err
}
return hex.EncodeToString(rootShaBytes), nil
}
type treeEntry struct {
mode string // like "100644"
name string // individual name
sha []byte
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
package main
import (
"encoding/hex"
"errors"
"fmt"
"io"
"net"
"net/http"
"strings"
"git.sr.ht/~sircmpwn/go-bare"
)
type commitDisplay struct {
Hash string
Author string
Email string
Date string
Message string
}
// httpHandleRepoIndex provides the front page of a repo using git2d.
func httpHandleRepoIndex(w http.ResponseWriter, req *http.Request, params map[string]any) {
repoName := params["repo_name"].(string)
groupPath := params["group_path"].([]string)
_, repoPath, _, _, _, _, _ := getRepoInfo(req.Context(), groupPath, repoName, "") // TODO: Don't use getRepoInfo
var notes []string
if strings.Contains(repoName, "\n") || sliceContainsNewlines(groupPath) {
notes = append(notes, "Path contains newlines; HTTP Git access impossible")
}
conn, err := net.Dial("unix", config.Git.Socket)
if err != nil {
errorPage500(w, params, "git2d connection failed: "+err.Error())
return
}
defer conn.Close()
writer := bare.NewWriter(conn)
reader := bare.NewReader(conn)
if err := writer.WriteData(stringToBytes(repoPath)); err != nil {
errorPage500(w, params, "sending repo path failed: "+err.Error())
return
}
if err := writer.WriteUint(1); err != nil {
errorPage500(w, params, "sending command failed: "+err.Error())
return
}
status, err := reader.ReadUint()
if err != nil {
errorPage500(w, params, "reading status failed: "+err.Error())
return
}
if status != 0 {
errorPage500(w, params, fmt.Sprintf("git2d error: %d", status))
return
}
// README
readmeRaw, err := reader.ReadData()
if err != nil {
readmeRaw = nil
}
readmeFilename, readmeRendered := renderReadme(readmeRaw, "README.md")
// Commits
var commits []commitDisplay
for {
id, err := reader.ReadData()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
errorPage500(w, params, "error reading commit ID: "+err.Error())
return
}
title, _ := reader.ReadData()
authorName, _ := reader.ReadData()
authorEmail, _ := reader.ReadData()
authorDate, _ := reader.ReadData()
commits = append(commits, commitDisplay{
Hash: hex.EncodeToString(id),
Author: string(authorName), Email: string(authorEmail), Date: string(authorDate), Message: string(title),
Author: bytesToString(authorName), Email: bytesToString(authorEmail), Date: bytesToString(authorDate), Message: bytesToString(title),
}) } params["commits"] = commits params["readme_filename"] = readmeFilename params["readme"] = readmeRendered params["notes"] = notes renderTemplate(w, "repo_index", params) // TODO: Caching }
// 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(stringToBytes(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(stringToBytes(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),
Name: bytesToString(name),
Mode: fmt.Sprintf("%06o", mode),
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))
fmt.Fprint(writer, bytesToString(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(stringToBytes(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(stringToBytes(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),
Name: bytesToString(name),
Mode: fmt.Sprintf("%06o", mode),
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))
rendered := renderHighlightedFile(pathSpec, bytesToString(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))
}
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
package main
import (
"bytes"
"html"
"html/template"
"strings"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/microcosm-cc/bluemonday"
"github.com/niklasfasching/go-org/org"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
)
var markdownConverter = goldmark.New(goldmark.WithExtensions(extension.GFM))
// escapeHTML just escapes a string and wraps it in [template.HTML].
func escapeHTML(s string) template.HTML {
return template.HTML(html.EscapeString(s)) //#nosec G203
}
// renderReadmeAtTree looks for README files in the supplied Git tree and
// returns its filename and rendered (and sanitized) HTML.
func renderReadmeAtTree(tree *object.Tree) (string, template.HTML) {
for _, name := range []string{"README", "README.md", "README.org"} {
file, err := tree.File(name)
if err != nil {
continue
}
contents, err := file.Contents()
if err != nil {
return "Error fetching README", escapeHTML("Unable to fetch contents of " + name + ": " + err.Error())
}
return renderReadme(stringToBytes(contents), name)
}
return "", ""
}
// renderReadme renders and sanitizes README content from a byte slice and filename.
func renderReadme(data []byte, filename string) (string, template.HTML) {
switch strings.ToLower(filename) {
case "readme":
return "README", template.HTML("<pre>" + html.EscapeString(string(data)) + "</pre>") //#nosec G203
return "README", template.HTML("<pre>" + html.EscapeString(bytesToString(data)) + "</pre>") //#nosec G203
case "readme.md":
var buf bytes.Buffer
if err := markdownConverter.Convert(data, &buf); err != nil {
return "Error fetching README", escapeHTML("Unable to render README: " + err.Error())
}
return "README.md", template.HTML(bluemonday.UGCPolicy().SanitizeBytes(buf.Bytes())) //#nosec G203
case "readme.org":
htmlStr, err := org.New().Parse(strings.NewReader(string(data)), filename).Write(org.NewHTMLWriter())
htmlStr, err := org.New().Parse(strings.NewReader(bytesToString(data)), filename).Write(org.NewHTMLWriter())
if err != nil {
return "Error fetching README", escapeHTML("Unable to render README: " + err.Error())
}
return "README.org", template.HTML(bluemonday.UGCPolicy().Sanitize(htmlStr)) //#nosec G203
default:
return filename, template.HTML("<pre>" + html.EscapeString(string(data)) + "</pre>") //#nosec G203
return filename, template.HTML("<pre>" + html.EscapeString(bytesToString(data)) + "</pre>") //#nosec G203
} }