Lindenii Project Forge
Login

server

Lindenii Forge’s main backend daemon
Commit info
ID
9b17278aece47aca17d32a37f67b7078708e78be
Author
Runxi Yu <me@runxiyu.org>
Author date
Sat, 05 Apr 2025 17:21:14 +0800
Committer
Runxi Yu <me@runxiyu.org>
Committer date
Sat, 05 Apr 2025 17:21:14 +0800
Actions
Refactor git2d comms to ./git2c
package git2c

import (
	"encoding/hex"
	"errors"
	"fmt"
	"io"
)

func (c *Client) Cmd1(repoPath string) ([]Commit, *FilenameContents, error) {
	if err := c.writer.WriteData([]byte(repoPath)); err != nil {
		return nil, nil, fmt.Errorf("sending repo path failed: %w", err)
	}
	if err := c.writer.WriteUint(1); err != nil {
		return nil, nil, fmt.Errorf("sending command failed: %w", err)
	}

	status, err := c.reader.ReadUint()
	if err != nil {
		return nil, nil, fmt.Errorf("reading status failed: %w", err)
	}
	if status != 0 {
		return nil, nil, fmt.Errorf("git2d error: %d", status)
	}

	// README
	readmeRaw, err := c.reader.ReadData()
	if err != nil {
		readmeRaw = nil
	}

	readmeFilename := "README.md" // TODO
	readme := &FilenameContents{Filename: readmeFilename, Content: readmeRaw}

	// Commits
	var commits []Commit
	for {
		id, err := c.reader.ReadData()
		if err != nil {
			if errors.Is(err, io.EOF) {
				break
			}
			return nil, nil, fmt.Errorf("reading commit ID failed: %w", err)
		}
		title, _ := c.reader.ReadData()
		authorName, _ := c.reader.ReadData()
		authorEmail, _ := c.reader.ReadData()
		authorDate, _ := c.reader.ReadData()

		commits = append(commits, Commit{
			Hash:    hex.EncodeToString(id),
			Author:  string(authorName),
			Email:   string(authorEmail),
			Date:    string(authorDate),
			Message: string(title),
		})
	}

	return commits, readme, nil
}
package git2c

import (
	"errors"
	"fmt"
	"io"
)

func (c *Client) Cmd2(repoPath, pathSpec string) ([]TreeEntry, string, error) {
	if err := c.writer.WriteData([]byte(repoPath)); err != nil {
		return nil, "", fmt.Errorf("sending repo path failed: %w", err)
	}
	if err := c.writer.WriteUint(2); err != nil {
		return nil, "", fmt.Errorf("sending command failed: %w", err)
	}
	if err := c.writer.WriteData([]byte(pathSpec)); err != nil {
		return nil, "", fmt.Errorf("sending path failed: %w", err)
	}

	status, err := c.reader.ReadUint()
	if err != nil {
		return nil, "", fmt.Errorf("reading status failed: %w", err)
	}

	switch status {
	case 0:
		kind, err := c.reader.ReadUint()
		if err != nil {
			return nil, "", fmt.Errorf("reading object kind failed: %w", err)
		}

		switch kind {
		case 1:
			// Tree
			count, err := c.reader.ReadUint()
			if err != nil {
				return nil, "", fmt.Errorf("reading entry count failed: %w", err)
			}

			var files []TreeEntry
			for i := 0; i < int(count); i++ {
				typeCode, err := c.reader.ReadUint()
				if err != nil {
					return nil, "", fmt.Errorf("error reading entry type: %w", err)
				}
				mode, err := c.reader.ReadUint()
				if err != nil {
					return nil, "", fmt.Errorf("error reading entry mode: %w", err)
				}
				size, err := c.reader.ReadUint()
				if err != nil {
					return nil, "", fmt.Errorf("error reading entry size: %w", err)
				}
				name, err := c.reader.ReadData()
				if err != nil {
					return nil, "", fmt.Errorf("error reading entry name: %w", err)
				}

				files = append(files, TreeEntry{
					Name:      string(name),
					Mode:      fmt.Sprintf("%06o", mode),
					Size:      size,
					IsFile:    typeCode == 2,
					IsSubtree: typeCode == 1,
				})
			}

			return files, "", nil

		case 2:
			// Blob
			content, err := c.reader.ReadData()
			if err != nil && !errors.Is(err, io.EOF) {
				return nil, "", fmt.Errorf("error reading file content: %w", err)
			}

			return nil, string(content), nil

		default:
			return nil, "", fmt.Errorf("unknown kind: %d", kind)
		}

	case 3:
		return nil, "", fmt.Errorf("path not found: %s", pathSpec)

	default:
		return nil, "", fmt.Errorf("unknown status code: %d", status)
	}
}
package git2c

import (
	"fmt"
	"net"

	"git.sr.ht/~sircmpwn/go-bare"
)

type Client struct {
	SocketPath string
	conn       net.Conn
	writer     *bare.Writer
	reader     *bare.Reader
}

func NewClient(socketPath string) (*Client, error) {
	conn, err := net.Dial("unix", socketPath)
	if err != nil {
		return nil, fmt.Errorf("git2d connection failed: %w", err)
	}

	writer := bare.NewWriter(conn)
	reader := bare.NewReader(conn)

	return &Client{
		SocketPath: socketPath,
		conn:       conn,
		writer:     writer,
		reader:     reader,
	}, nil
}

func (c *Client) Close() error {
	if c.conn != nil {
		return c.conn.Close()
	}
	return nil
}
package git2c

type Commit struct {
	Hash    string
	Author  string
	Email   string
	Date    string
	Message string
}

type FilenameContents struct {
	Filename string
	Content  []byte
}

type TreeEntry struct {
	Name      string
	Mode      string
	Size      uint64
	IsFile    bool
	IsSubtree bool
}
// 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"
	"go.lindenii.runxiyu.org/forge/git2c"
)

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)
	client, err := git2c.NewClient(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())
		errorPage500(w, params, err.Error())
		return
	}
	defer client.Close()

	if err := writer.WriteUint(1); err != nil {
		errorPage500(w, params, "sending command failed: "+err.Error())
		return
	}

	status, err := reader.ReadUint()
	commits, readme, err := client.Cmd1(repoPath)
	if err != nil {
		errorPage500(w, params, "reading status failed: "+err.Error())
		return
	}
	if status != 0 {
		errorPage500(w, params, fmt.Sprintf("git2d error: %d", status))
		errorPage500(w, params, err.Error())
		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:  bytesToString(authorName),
			Email:   bytesToString(authorEmail),
			Date:    bytesToString(authorDate),
			Message: bytesToString(title),
		})
	}

	params["commits"] = commits
	params["readme_filename"] = readmeFilename
	params["readme"] = readmeRendered
	params["readme_filename"] = readme.Filename
	_, params["readme"] = renderReadme(readme.Content, readme.Filename)
	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"
	"go.lindenii.runxiyu.org/forge/git2c"
)

// 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)
	client, err := git2c.NewClient(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())
		errorPage500(writer, params, err.Error())
		return
	}
	if err := brWriter.WriteData(stringToBytes(pathSpec)); err != nil {
		errorPage500(writer, params, "sending path failed: "+err.Error())
		return
	}
	defer client.Close()

	status, err := brReader.ReadUint()
	files, content, err := client.Cmd2(repoPath, pathSpec)
	if err != nil {
		errorPage500(writer, params, "reading status failed: "+err.Error())
		errorPage500(writer, params, err.Error())
		return
	}

	switch status {
	case 0:
		kind, err := brReader.ReadUint()
		if err != nil {
			errorPage500(writer, params, "reading object kind failed: "+err.Error())
	if files != nil {
		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)
	} else if content != "" {
		if redirectNoDir(writer, request) {
			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:      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, 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))
		writer.Header().Set("Content-Type", "application/octet-stream")
		fmt.Fprint(writer, content)
	} else {
		errorPage500(writer, params, "Unknown error fetching repo raw data")
	}
}
// 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"
	"go.lindenii.runxiyu.org/forge/git2c"
)

// 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)
	client, err := git2c.NewClient(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())
		errorPage500(writer, params, 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
	}
	defer client.Close()

	status, err := brReader.ReadUint()
	files, content, err := client.Cmd2(repoPath, pathSpec)
	if err != nil {
		errorPage500(writer, params, "reading status failed: "+err.Error())
		errorPage500(writer, params, 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:      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, 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))
	if files != nil {
		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)
	} else if content != "" {
		rendered := renderHighlightedFile(pathSpec, content)
		params["file_contents"] = rendered
		renderTemplate(writer, "repo_tree_file", params)
	} else {
		errorPage500(writer, params, "Unknown object type, something is seriously wrong")
	}
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>

package main

import (
	"embed"
	"html/template"
	"io/fs"
	"net/http"

	"github.com/tdewolff/minify/v2"
	"github.com/tdewolff/minify/v2/html"
)

//go:embed LICENSE source.tar.gz
var sourceFS embed.FS

var sourceHandler = http.StripPrefix(
	"/-/source/",
	http.FileServer(http.FS(sourceFS)),
)

//go:embed templates/* static/*
//go:embed hookc/hookc git2d/git2d
var resourcesFS embed.FS

var templates *template.Template

// loadTemplates minifies and loads HTML templates.
func loadTemplates() (err error) {
	minifier := minify.New()
	minifierOptions := html.Minifier{
		TemplateDelims:      [2]string{"{{", "}}"},
		KeepDefaultAttrVals: true,
	} //exhaustruct:ignore
	minifier.Add("text/html", &minifierOptions)

	templates = template.New("templates").Funcs(template.FuncMap{
		"first_line":        firstLine,
		"path_escape":       pathEscape,
		"query_escape":      queryEscape,
		"dereference_error": dereferenceOrZero[error],
		"minus":             minus,
	})

	err = fs.WalkDir(resourcesFS, "templates", func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if !d.IsDir() {
			content, err := fs.ReadFile(resourcesFS, path)
			if err != nil {
				return err
			}

			minified, err := minifier.Bytes("text/html", content)
			if err != nil {
				return err
			}

			_, err = templates.Parse(bytesToString(minified))
			if err != nil {
				return err
			}
		}
		return nil
	})
	return err
}

var (
	staticHandler http.Handler
)
var staticHandler http.Handler

// This init sets up static handlers. The resulting handlers must be
// used in the HTTP router, and do nothing unless called from elsewhere.
func init() {
	staticFS, err := fs.Sub(resourcesFS, "static")
	if err != nil {
		panic(err)
	}
	staticHandler = http.StripPrefix("/-/static/", http.FileServer(http.FS(staticFS)))
}