add results in binary encoding for events endpoint

This commit is contained in:
2025-04-27 15:11:46 -01:06
parent a7e51f3353
commit 7d245fbe8e
47 changed files with 73 additions and 3013 deletions

View File

@@ -1,52 +0,0 @@
name: build and push docker image
on:
push:
branches: [master]
tags: ["*"]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Copy Dockerfile to .
run: cp ./contrib/Dockerfile ./Dockerfile
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
push: true
build-args: |
BUILDKIT_INLINE_CACHE=1

View File

@@ -1,20 +0,0 @@
repo:
scanPath: /var/www/git
readme:
- readme
- README
- readme.md
- README.md
mainBranch:
- master
- main
dirs:
templates: ./templates
static: ./static
meta:
title: icy does git
description: come get your free software
server:
name: git.icyphox.sh
host: 0.0.0.0
port: 5555

View File

@@ -1,57 +0,0 @@
package config
import (
"fmt"
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
type Config struct {
Repo struct {
ScanPath string `yaml:"scanPath"`
Readme []string `yaml:"readme"`
MainBranch []string `yaml:"mainBranch"`
Ignore []string `yaml:"ignore,omitempty"`
Unlisted []string `yaml:"unlisted,omitempty"`
} `yaml:"repo"`
Dirs struct {
Templates string `yaml:"templates"`
Static string `yaml:"static"`
} `yaml:"dirs"`
Meta struct {
Title string `yaml:"title"`
Description string `yaml:"description"`
SyntaxHighlight string `yaml:"syntaxHighlight"`
} `yaml:"meta"`
Server struct {
Name string `yaml:"name,omitempty"`
Host string `yaml:"host"`
Port int `yaml:"port"`
} `yaml:"server"`
}
func Read(f string) (*Config, error) {
b, err := os.ReadFile(f)
if err != nil {
return nil, fmt.Errorf("reading config: %w", err)
}
c := Config{}
if err := yaml.Unmarshal(b, &c); err != nil {
return nil, fmt.Errorf("parsing config: %w", err)
}
if c.Repo.ScanPath, err = filepath.Abs(c.Repo.ScanPath); err != nil {
return nil, err
}
if c.Dirs.Templates, err = filepath.Abs(c.Dirs.Templates); err != nil {
return nil, err
}
if c.Dirs.Static, err = filepath.Abs(c.Dirs.Static); err != nil {
return nil, err
}
return &c, nil
}

View File

@@ -1,22 +0,0 @@
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN go mod download
RUN go mod verify
RUN go build -o legit
FROM scratch AS build-release-stage
WORKDIR /app
COPY static ./static
COPY templates ./templates
COPY config.yaml ./
COPY --from=builder /app/legit ./
EXPOSE 5555
CMD ["./legit"]

View File

@@ -1,14 +0,0 @@
services:
legit:
container_name: legit
build:
context: ../
dockerfile: contrib/Dockerfile
restart: unless-stopped
ports:
- "5555:5555"
volumes:
- /var/www/git:/var/www/git
- ../config.yaml:/app/config.yaml
- ../static:/app/static
- ../templates:/app/templates

View File

@@ -1,17 +0,0 @@
[Unit]
Description=legit Server
After=network-online.target
Requires=network-online.target
[Service]
User=git
Group=git
ExecStart=/usr/bin/legit -config /etc/legit/config.yaml
ProtectSystem=strict
ProtectHome=strict
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
[Install]
WantedBy=multi-user.target

26
cmd/legit/flake.lock generated
View File

@@ -1,26 +0,0 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1718558927,
"narHash": "sha256-PRqvkPqX5luuZ0WcUbz2zATGp4IzybDU0K33MxO9Sd0=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "f82fe275d98c521c051af4892cd8b3406cee67a3",
"type": "github"
},
"original": {
"owner": "nixos",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -1,62 +0,0 @@
{
description = "web frontend for git";
inputs.nixpkgs.url = "github:nixos/nixpkgs";
outputs =
{ self
, nixpkgs
,
}:
let
supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
in
{
packages = forAllSystems (system:
let
pkgs = nixpkgsFor.${system};
legit = self.packages.${system}.legit;
files = pkgs.lib.fileset.toSource {
root = ./.;
fileset = pkgs.lib.fileset.unions [
./config.yaml
./static
./templates
];
};
in
{
legit = pkgs.buildGoModule {
name = "legit";
rev = "master";
src = ./.;
vendorHash = "sha256-ynv0pBdVPIhTz7RvCwVWr0vUWwfw+PEjFXs9PdQMqm8=";
};
docker = pkgs.dockerTools.buildLayeredImage {
name = "sini:5000/legit";
tag = "latest";
contents = [ files legit pkgs.git ];
config = {
Entrypoint = [ "${legit}/bin/legit" ];
ExposedPorts = { "5555/tcp" = { }; };
};
};
});
defaultPackage = forAllSystems (system: self.packages.${system}.legit);
devShells = forAllSystems (system:
let
pkgs = nixpkgsFor.${system};
in
{
default = pkgs.mkShell {
nativeBuildInputs = with pkgs; [
go
];
};
});
};
}

View File

@@ -1,119 +0,0 @@
package git
import (
"fmt"
"log"
"strings"
"github.com/bluekeyes/go-gitdiff/gitdiff"
"github.com/go-git/go-git/v5/plumbing/object"
)
type TextFragment struct {
Header string
Lines []gitdiff.Line
}
type Diff struct {
Name struct {
Old string
New string
}
TextFragments []TextFragment
IsBinary bool
IsNew bool
IsDelete bool
}
// A nicer git diff representation.
type NiceDiff struct {
Commit struct {
Message string
Author object.Signature
This string
Parent string
}
Stat struct {
FilesChanged int
Insertions int
Deletions int
}
Diff []Diff
}
func (g *GitRepo) Diff() (*NiceDiff, error) {
c, err := g.r.CommitObject(g.h)
if err != nil {
return nil, fmt.Errorf("commit object: %w", err)
}
patch := &object.Patch{}
commitTree, err := c.Tree()
parent := &object.Commit{}
if err == nil {
parentTree := &object.Tree{}
if c.NumParents() != 0 {
parent, err = c.Parents().Next()
if err == nil {
parentTree, err = parent.Tree()
if err == nil {
patch, err = parentTree.Patch(commitTree)
if err != nil {
return nil, fmt.Errorf("patch: %w", err)
}
}
}
} else {
patch, err = parentTree.Patch(commitTree)
if err != nil {
return nil, fmt.Errorf("patch: %w", err)
}
}
}
diffs, _, err := gitdiff.Parse(strings.NewReader(patch.String()))
if err != nil {
log.Println(err)
}
nd := NiceDiff{}
nd.Commit.This = c.Hash.String()
if parent.Hash.IsZero() {
nd.Commit.Parent = ""
} else {
nd.Commit.Parent = parent.Hash.String()
}
nd.Commit.Author = c.Author
nd.Commit.Message = c.Message
for _, d := range diffs {
ndiff := Diff{}
ndiff.Name.New = d.NewName
ndiff.Name.Old = d.OldName
ndiff.IsBinary = d.IsBinary
ndiff.IsNew = d.IsNew
ndiff.IsDelete = d.IsDelete
for _, tf := range d.TextFragments {
ndiff.TextFragments = append(ndiff.TextFragments, TextFragment{
Header: tf.Header(),
Lines: tf.Lines,
})
for _, l := range tf.Lines {
switch l.Op {
case gitdiff.OpAdd:
nd.Stat.Insertions += 1
case gitdiff.OpDelete:
nd.Stat.Deletions += 1
}
}
}
nd.Diff = append(nd.Diff, ndiff)
}
nd.Stat.FilesChanged = len(diffs)
return &nd, nil
}

View File

@@ -1,344 +0,0 @@
package git
import (
"archive/tar"
"fmt"
"io"
"io/fs"
"path"
"sort"
"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"
)
type GitRepo struct {
r *git.Repository
h plumbing.Hash
}
type TagList struct {
refs []*TagReference
r *git.Repository
}
// TagReference is used to list both tag and non-annotated tags.
// Non-annotated tags should only contains a reference.
// Annotated tags should contain its reference and its tag information.
type TagReference struct {
ref *plumbing.Reference
tag *object.Tag
}
// infoWrapper wraps the property of a TreeEntry so it can export fs.FileInfo
// to tar WriteHeader
type infoWrapper struct {
name string
size int64
mode fs.FileMode
modTime time.Time
isDir bool
}
func (self *TagList) Len() int {
return len(self.refs)
}
func (self *TagList) Swap(i, j int) {
self.refs[i], self.refs[j] = self.refs[j], self.refs[i]
}
// sorting tags in reverse chronological order
func (self *TagList) Less(i, j int) bool {
var dateI time.Time
var dateJ time.Time
if self.refs[i].tag != nil {
dateI = self.refs[i].tag.Tagger.When
} else {
c, err := self.r.CommitObject(self.refs[i].ref.Hash())
if err != nil {
dateI = time.Now()
} else {
dateI = c.Committer.When
}
}
if self.refs[j].tag != nil {
dateJ = self.refs[j].tag.Tagger.When
} else {
c, err := self.r.CommitObject(self.refs[j].ref.Hash())
if err != nil {
dateJ = time.Now()
} else {
dateJ = c.Committer.When
}
}
return dateI.After(dateJ)
}
func Open(path string, ref string) (*GitRepo, error) {
var err error
g := GitRepo{}
g.r, err = git.PlainOpen(path)
if err != nil {
return nil, fmt.Errorf("opening %s: %w", path, err)
}
if ref == "" {
head, err := g.r.Head()
if err != nil {
return nil, fmt.Errorf("getting head of %s: %w", path, err)
}
g.h = head.Hash()
} else {
hash, err := g.r.ResolveRevision(plumbing.Revision(ref))
if err != nil {
return nil, fmt.Errorf("resolving rev %s for %s: %w", ref, path, err)
}
g.h = *hash
}
return &g, nil
}
func (g *GitRepo) Commits() ([]*object.Commit, error) {
ci, err := g.r.Log(&git.LogOptions{From: g.h})
if err != nil {
return nil, fmt.Errorf("commits from ref: %w", err)
}
commits := []*object.Commit{}
ci.ForEach(func(c *object.Commit) error {
commits = append(commits, c)
return nil
})
return commits, nil
}
func (g *GitRepo) LastCommit() (*object.Commit, error) {
c, err := g.r.CommitObject(g.h)
if err != nil {
return nil, fmt.Errorf("last commit: %w", err)
}
return c, nil
}
func (g *GitRepo) FileContent(path string) (string, error) {
c, err := g.r.CommitObject(g.h)
if err != nil {
return "", fmt.Errorf("commit object: %w", err)
}
tree, err := c.Tree()
if err != nil {
return "", fmt.Errorf("file tree: %w", err)
}
file, err := tree.File(path)
if err != nil {
return "", err
}
isbin, _ := file.IsBinary()
if !isbin {
return file.Contents()
} else {
return "Not displaying binary file", nil
}
}
func (g *GitRepo) Tags() ([]*TagReference, error) {
iter, err := g.r.Tags()
if err != nil {
return nil, fmt.Errorf("tag objects: %w", err)
}
tags := make([]*TagReference, 0)
if err := iter.ForEach(func(ref *plumbing.Reference) error {
obj, err := g.r.TagObject(ref.Hash())
switch err {
case nil:
tags = append(tags, &TagReference{
ref: ref,
tag: obj,
})
case plumbing.ErrObjectNotFound:
tags = append(tags, &TagReference{
ref: ref,
})
default:
return err
}
return nil
}); err != nil {
return nil, err
}
tagList := &TagList{r: g.r, refs: tags}
sort.Sort(tagList)
return tags, nil
}
func (g *GitRepo) Branches() ([]*plumbing.Reference, error) {
bi, err := g.r.Branches()
if err != nil {
return nil, fmt.Errorf("branchs: %w", err)
}
branches := []*plumbing.Reference{}
_ = bi.ForEach(func(ref *plumbing.Reference) error {
branches = append(branches, ref)
return nil
})
return branches, nil
}
func (g *GitRepo) FindMainBranch(branches []string) (string, error) {
for _, b := range branches {
_, err := g.r.ResolveRevision(plumbing.Revision(b))
if err == nil {
return b, nil
}
}
return "", fmt.Errorf("unable to find main branch")
}
// WriteTar writes itself from a tree into a binary tar file format.
// prefix is root folder to be appended.
func (g *GitRepo) WriteTar(w io.Writer, prefix string) error {
tw := tar.NewWriter(w)
defer tw.Close()
c, err := g.r.CommitObject(g.h)
if err != nil {
return fmt.Errorf("commit object: %w", err)
}
tree, err := c.Tree()
if err != nil {
return err
}
walker := object.NewTreeWalker(tree, true, nil)
defer walker.Close()
name, entry, err := walker.Next()
for ; err == nil; name, entry, err = walker.Next() {
info, err := newInfoWrapper(name, prefix, &entry, tree)
if err != nil {
return err
}
header, err := tar.FileInfoHeader(info, "")
if err != nil {
return err
}
err = tw.WriteHeader(header)
if err != nil {
return err
}
if !info.IsDir() {
file, err := tree.File(name)
if err != nil {
return err
}
reader, err := file.Blob.Reader()
if err != nil {
return err
}
_, err = io.Copy(tw, reader)
if err != nil {
reader.Close()
return err
}
reader.Close()
}
}
return nil
}
func newInfoWrapper(
name string,
prefix string,
entry *object.TreeEntry,
tree *object.Tree,
) (*infoWrapper, error) {
var (
size int64
mode fs.FileMode
isDir bool
)
if entry.Mode.IsFile() {
file, err := tree.TreeEntryFile(entry)
if err != nil {
return nil, err
}
mode = fs.FileMode(file.Mode)
size, err = tree.Size(name)
if err != nil {
return nil, err
}
} else {
isDir = true
mode = fs.ModeDir | fs.ModePerm
}
fullname := path.Join(prefix, name)
return &infoWrapper{
name: fullname,
size: size,
mode: mode,
modTime: time.Unix(0, 0),
isDir: isDir,
}, nil
}
func (i *infoWrapper) Name() string {
return i.name
}
func (i *infoWrapper) Size() int64 {
return i.size
}
func (i *infoWrapper) Mode() fs.FileMode {
return i.mode
}
func (i *infoWrapper) ModTime() time.Time {
return i.modTime
}
func (i *infoWrapper) IsDir() bool {
return i.isDir
}
func (i *infoWrapper) Sys() any {
return nil
}
func (t *TagReference) Name() string {
return t.ref.Name().Short()
}
func (t *TagReference) Message() string {
if t.tag != nil {
return t.tag.Message
}
return ""
}

View File

@@ -1,121 +0,0 @@
package service
import (
"bytes"
"fmt"
"io"
"log"
"net/http"
"os/exec"
"strings"
"syscall"
)
// Mostly from charmbracelet/soft-serve and sosedoff/gitkit.
type ServiceCommand struct {
Dir string
Stdin io.Reader
Stdout http.ResponseWriter
}
func (c *ServiceCommand) InfoRefs() error {
cmd := exec.Command("git", []string{
"upload-pack",
"--stateless-rpc",
"--advertise-refs",
".",
}...)
cmd.Dir = c.Dir
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
stdoutPipe, _ := cmd.StdoutPipe()
cmd.Stderr = cmd.Stdout
if err := cmd.Start(); err != nil {
log.Printf("git: failed to start git-upload-pack (info/refs): %s", err)
return err
}
if err := packLine(c.Stdout, "# service=git-upload-pack\n"); err != nil {
log.Printf("git: failed to write pack line: %s", err)
return err
}
if err := packFlush(c.Stdout); err != nil {
log.Printf("git: failed to flush pack: %s", err)
return err
}
buf := bytes.Buffer{}
if _, err := io.Copy(&buf, stdoutPipe); err != nil {
log.Printf("git: failed to copy stdout to tmp buffer: %s", err)
return err
}
if err := cmd.Wait(); err != nil {
out := strings.Builder{}
_, _ = io.Copy(&out, &buf)
log.Printf("git: failed to run git-upload-pack; err: %s; output: %s", err, out.String())
return err
}
if _, err := io.Copy(c.Stdout, &buf); err != nil {
log.Printf("git: failed to copy stdout: %s", err)
}
return nil
}
func (c *ServiceCommand) UploadPack() error {
cmd := exec.Command("git", []string{
"-c", "uploadpack.allowFilter=true",
"upload-pack",
"--stateless-rpc",
".",
}...)
cmd.Dir = c.Dir
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
stdoutPipe, _ := cmd.StdoutPipe()
cmd.Stderr = cmd.Stdout
defer stdoutPipe.Close()
stdinPipe, err := cmd.StdinPipe()
if err != nil {
return err
}
defer stdinPipe.Close()
if err := cmd.Start(); err != nil {
log.Printf("git: failed to start git-upload-pack: %s", err)
return err
}
if _, err := io.Copy(stdinPipe, c.Stdin); err != nil {
log.Printf("git: failed to copy stdin: %s", err)
return err
}
stdinPipe.Close()
if _, err := io.Copy(newWriteFlusher(c.Stdout), stdoutPipe); err != nil {
log.Printf("git: failed to copy stdout: %s", err)
return err
}
if err := cmd.Wait(); err != nil {
log.Printf("git: failed to wait for git-upload-pack: %s", err)
return err
}
return nil
}
func packLine(w io.Writer, s string) error {
_, err := fmt.Fprintf(w, "%04x%s", len(s)+4, s)
return err
}
func packFlush(w io.Writer) error {
_, err := fmt.Fprint(w, "0000")
return err
}

View File

@@ -1,25 +0,0 @@
package service
import (
"io"
"net/http"
)
func newWriteFlusher(w http.ResponseWriter) io.Writer {
return writeFlusher{w.(interface {
io.Writer
http.Flusher
})}
}
type writeFlusher struct {
wf interface {
io.Writer
http.Flusher
}
}
func (w writeFlusher) Write(p []byte) (int, error) {
defer w.wf.Flush()
return w.wf.Write(p)
}

View File

@@ -1,66 +0,0 @@
package git
import (
"fmt"
"github.com/go-git/go-git/v5/plumbing/object"
)
func (g *GitRepo) FileTree(path string) ([]NiceTree, error) {
c, err := g.r.CommitObject(g.h)
if err != nil {
return nil, fmt.Errorf("commit object: %w", err)
}
files := []NiceTree{}
tree, err := c.Tree()
if err != nil {
return nil, fmt.Errorf("file tree: %w", err)
}
if path == "" {
files = makeNiceTree(tree)
} else {
o, err := tree.FindEntry(path)
if err != nil {
return nil, err
}
if !o.Mode.IsFile() {
subtree, err := tree.Tree(path)
if err != nil {
return nil, err
}
files = makeNiceTree(subtree)
}
}
return files, nil
}
// A nicer git tree representation.
type NiceTree struct {
Name string
Mode string
Size int64
IsFile bool
IsSubtree bool
}
func makeNiceTree(t *object.Tree) []NiceTree {
nts := []NiceTree{}
for _, e := range t.Entries {
mode, _ := e.Mode.ToOSFileMode()
sz, _ := t.Size(e.Name)
nts = append(nts, NiceTree{
Name: e.Name,
Mode: mode.String(),
IsFile: e.Mode.IsFile(),
Size: sz,
})
}
return nts
}

View File

@@ -1,42 +0,0 @@
module realy.lol/cmd/legit
go 1.22.0
require (
github.com/alecthomas/chroma/v2 v2.14.0
github.com/bluekeyes/go-gitdiff v0.8.0
github.com/cyphar/filepath-securejoin v0.4.1
github.com/dustin/go-humanize v1.0.1
github.com/go-git/go-git/v5 v5.13.2
github.com/microcosm-cc/bluemonday v1.0.27
github.com/russross/blackfriday/v2 v2.1.0
golang.org/x/sys v0.30.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.5 // indirect
github.com/acomagu/bufpipe v1.0.4 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/cloudflare/circl v1.6.0 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/sergi/go-diff v1.3.1 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/net v0.35.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)
replace github.com/sergi/go-diff => github.com/sergi/go-diff v1.1.0
replace github.com/go-git/go-git/v5 => github.com/go-git/go-git/v5 v5.6.1

View File

@@ -1,187 +0,0 @@
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ=
github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/bluekeyes/go-gitdiff v0.8.0 h1:Nn1wfw3/XeKoc3lWk+2bEXGUHIx36kj80FM1gVcBk+o=
github.com/bluekeyes/go-gitdiff v0.8.0/go.mod h1:WWAk1Mc6EgWarCrPFO+xeYlujPu98VuLW3Tu+B/85AE=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git-fixtures/v4 v4.3.1 h1:y5z6dd3qi8Hl+stezc8p3JxDkoTRqMAlKnXHuzrfjTQ=
github.com/go-git/go-git-fixtures/v4 v4.3.1/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo=
github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk=
github.com/go-git/go-git/v5 v5.6.1/go.mod h1:mvyoL6Unz0PiTQrGQfSfiLFhBH1c1e84ylC2MDs4ee8=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/mmcloughlin/avo v0.5.0/go.mod h1:ChHFdoV7ql95Wi7vuq2YT1bwCJqiWdZrQ1im3VujLYM=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.1.0/go.mod h1:sKFq3RD6/TKZkSWn8boUbDC7Qkgcv+8XXijpFO6roag=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/arch v0.1.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -1,23 +0,0 @@
The MIT License (MIT)
Copyright (c) Anirudh Oppiliappan <x@icyphox.sh>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,36 +0,0 @@
package main
import (
"flag"
"fmt"
"log"
"net/http"
"realy.lol/cmd/legit/config"
"realy.lol/cmd/legit/routes"
)
func main() {
var cfg string
flag.StringVar(&cfg, "config", "./config.yaml", "path to config file")
flag.Parse()
c, err := config.Read(cfg)
if err != nil {
log.Fatal(err)
}
if err := UnveilPaths([]string{
c.Dirs.Static,
c.Repo.ScanPath,
c.Dirs.Templates,
},
"r"); err != nil {
log.Fatalf("unveil: %s", err)
}
mux := routes.Handlers(c)
addr := fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port)
log.Println("starting server on", addr)
log.Fatal(http.ListenAndServe(addr, mux))
}

View File

@@ -1,88 +0,0 @@
legit
-----
A git web frontend written in Go.
Pronounced however you like; I prefer channeling my inner beret-wearing
Frenchman, and saying "Oui, il est le git!"
But yeah it's pretty legit, no cap on god fr fr.
FEATURES
• Fully customizable templates and stylesheets.
• Cloning over http(s).
• Less archaic HTML.
• Not CGI.
INSTALLING
Clone it, 'go build' it.
CONFIG
Uses yaml for configuration. Looks for a 'config.yaml' in the current
directory by default; pass the '--config' flag to point it elsewhere.
Example config.yaml:
repo:
scanPath: /var/www/git
readme:
- readme
- README
- readme.md
- README.md
mainBranch:
- master
- main
ignore:
- foo
- bar
dirs:
templates: ./templates
static: ./static
meta:
title: git good
description: i think it's a skill issue
syntaxHighlight: monokailight
server:
name: git.icyphox.sh
host: 127.0.0.1
port: 5555
These options are fairly self-explanatory, but of note are:
• repo.scanPath: where all your git repos live (or die). legit doesn't
traverse subdirs yet.
• dirs: use this to override the default templates and static assets.
• repo.readme: readme files to look for.
• repo.mainBranch: main branch names to look for.
• repo.ignore: repos to ignore, relative to scanPath.
• repo.unlisted: repos to hide, relative to scanPath.
• server.name: used for go-import meta tags and clone URLs.
• meta.syntaxHighlight: this is used to select the syntax theme to render. If left
blank or removed, the native theme will be used. If an invalid theme is set in this field,
it will default to "monokailight". For more information
about themes, please refer to chroma's gallery [1].
NOTES
• Run legit behind a TLS terminating proxy like relayd(8) or nginx.
• Cloning only works in bare repos -- this is a limitation inherent to git. You
can still view non-bare repos just fine in legit.
• Pushing over https, while supported, is disabled because auth is a
pain. Use ssh.
• Paths are unveil(2)'d on OpenBSD.
• Docker images are available ghcr.io/icyphox/legit:{master,latest,vX.Y.Z}. [2]
LICENSE
legit is licensed under MIT.
[1]: https://swapoff.org/chroma/playground/
[2]: https://github.com/icyphox/legit/pkgs/container/legit

View File

@@ -1,81 +0,0 @@
package routes
import (
"compress/gzip"
"io"
"log"
"net/http"
"path/filepath"
securejoin "github.com/cyphar/filepath-securejoin"
"realy.lol/cmd/legit/git/service"
)
func (d *deps) InfoRefs(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
name = filepath.Clean(name)
repo, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
if err != nil {
log.Printf("securejoin error: %v", err)
d.Write404(w)
return
}
w.Header().Set("content-type", "application/x-git-upload-pack-advertisement")
w.WriteHeader(http.StatusOK)
cmd := service.ServiceCommand{
Dir: repo,
Stdout: w,
}
if err := cmd.InfoRefs(); err != nil {
http.Error(w, err.Error(), 500)
log.Printf("git: failed to execute git-upload-pack (info/refs) %s", err)
return
}
}
func (d *deps) UploadPack(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
name = filepath.Clean(name)
repo, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
if err != nil {
log.Printf("securejoin error: %v", err)
d.Write404(w)
return
}
w.Header().Set("content-type", "application/x-git-upload-pack-result")
w.Header().Set("Connection", "Keep-Alive")
w.Header().Set("Transfer-Encoding", "chunked")
w.WriteHeader(http.StatusOK)
cmd := service.ServiceCommand{
Dir: repo,
Stdout: w,
}
var reader io.ReadCloser
reader = r.Body
if r.Header.Get("Content-Encoding") == "gzip" {
reader, err := gzip.NewReader(r.Body)
if err != nil {
http.Error(w, err.Error(), 500)
log.Printf("git: failed to create gzip reader: %s", err)
return
}
defer reader.Close()
}
cmd.Stdin = reader
if err := cmd.UploadPack(); err != nil {
http.Error(w, err.Error(), 500)
log.Printf("git: failed to execute git-upload-pack %s", err)
return
}
}

View File

@@ -1,49 +0,0 @@
package routes
import (
"net/http"
"realy.lol/cmd/legit/config"
)
// Checks for gitprotocol-http(5) specific smells; if found, passes
// the request on to the git http service, else render the web frontend.
func (d *deps) Multiplex(w http.ResponseWriter, r *http.Request) {
path := r.PathValue("rest")
if r.URL.RawQuery == "service=git-receive-pack" {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("no pushing allowed!"))
return
}
if path == "info/refs" &&
r.URL.RawQuery == "service=git-upload-pack" &&
r.Method == "GET" {
d.InfoRefs(w, r)
} else if path == "git-upload-pack" && r.Method == "POST" {
d.UploadPack(w, r)
} else if r.Method == "GET" {
d.RepoIndex(w, r)
}
}
func Handlers(c *config.Config) *http.ServeMux {
mux := http.NewServeMux()
d := deps{c}
mux.HandleFunc("GET /", d.Index)
mux.HandleFunc("GET /static/{file}", d.ServeStatic)
mux.HandleFunc("GET /{name}", d.Multiplex)
mux.HandleFunc("POST /{name}", d.Multiplex)
mux.HandleFunc("GET /{name}/tree/{ref}/{rest...}", d.RepoTree)
mux.HandleFunc("GET /{name}/blob/{ref}/{rest...}", d.FileContent)
mux.HandleFunc("GET /{name}/log/{ref}", d.Log)
mux.HandleFunc("GET /{name}/archive/{file}", d.Archive)
mux.HandleFunc("GET /{name}/commit/{ref}", d.Diff)
mux.HandleFunc("GET /{name}/refs/{$}", d.Refs)
mux.HandleFunc("GET /{name}/{rest...}", d.Multiplex)
mux.HandleFunc("POST /{name}/{rest...}", d.Multiplex)
return mux
}

View File

@@ -1,486 +0,0 @@
package routes
import (
"compress/gzip"
"fmt"
"html/template"
"log"
"net/http"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
securejoin "github.com/cyphar/filepath-securejoin"
"github.com/dustin/go-humanize"
"github.com/microcosm-cc/bluemonday"
"github.com/russross/blackfriday/v2"
"realy.lol/cmd/legit/config"
"realy.lol/cmd/legit/git"
)
type deps struct {
c *config.Config
}
func (d *deps) Index(w http.ResponseWriter, r *http.Request) {
dirs, err := os.ReadDir(d.c.Repo.ScanPath)
if err != nil {
d.Write500(w)
log.Printf("reading scan path: %s", err)
return
}
type info struct {
DisplayName, Name, Desc, Idle string
d time.Time
}
infos := []info{}
for _, dir := range dirs {
name := dir.Name()
if !dir.IsDir() || d.isIgnored(name) || d.isUnlisted(name) {
continue
}
path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
if err != nil {
log.Printf("securejoin error: %v", err)
d.Write404(w)
return
}
gr, err := git.Open(path, "")
if err != nil {
log.Println(err)
continue
}
c, err := gr.LastCommit()
if err != nil {
d.Write500(w)
log.Println(err)
return
}
infos = append(infos, info{
DisplayName: getDisplayName(name),
Name: name,
Desc: getDescription(path),
Idle: humanize.Time(c.Author.When),
d: c.Author.When,
})
}
sort.Slice(infos, func(i, j int) bool {
return infos[j].d.Before(infos[i].d)
})
tpath := filepath.Join(d.c.Dirs.Templates, "*")
t := template.Must(template.ParseGlob(tpath))
data := make(map[string]interface{})
data["meta"] = d.c.Meta
data["info"] = infos
if err := t.ExecuteTemplate(w, "index", data); err != nil {
log.Println(err)
return
}
}
func (d *deps) RepoIndex(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
if d.isIgnored(name) {
d.Write404(w)
return
}
name = filepath.Clean(name)
path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
if err != nil {
log.Printf("securejoin error: %v", err)
d.Write404(w)
return
}
gr, err := git.Open(path, "")
if err != nil {
d.Write404(w)
return
}
commits, err := gr.Commits()
if err != nil {
d.Write500(w)
log.Println(err)
return
}
var readmeContent template.HTML
for _, readme := range d.c.Repo.Readme {
ext := filepath.Ext(readme)
content, _ := gr.FileContent(readme)
if len(content) > 0 {
switch ext {
case ".md", ".mkd", ".markdown":
unsafe := blackfriday.Run(
[]byte(content),
blackfriday.WithExtensions(blackfriday.CommonExtensions),
)
html := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
readmeContent = template.HTML(html)
default:
safe := bluemonday.UGCPolicy().SanitizeBytes([]byte(content))
readmeContent = template.HTML(
fmt.Sprintf(`<pre>%s</pre>`, safe),
)
}
break
}
}
if readmeContent == "" {
log.Printf("no readme found for %s", name)
}
mainBranch, err := gr.FindMainBranch(d.c.Repo.MainBranch)
if err != nil {
d.Write500(w)
log.Println(err)
return
}
tpath := filepath.Join(d.c.Dirs.Templates, "*")
t := template.Must(template.ParseGlob(tpath))
if len(commits) >= 3 {
commits = commits[:3]
}
data := make(map[string]any)
data["name"] = name
data["displayname"] = getDisplayName(name)
data["ref"] = mainBranch
data["readme"] = readmeContent
data["commits"] = commits
data["desc"] = getDescription(path)
data["servername"] = d.c.Server.Name
data["meta"] = d.c.Meta
data["gomod"] = isGoModule(gr)
if err := t.ExecuteTemplate(w, "repo", data); err != nil {
log.Println(err)
return
}
return
}
func (d *deps) RepoTree(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
if d.isIgnored(name) {
d.Write404(w)
return
}
treePath := r.PathValue("rest")
ref := r.PathValue("ref")
name = filepath.Clean(name)
path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
if err != nil {
log.Printf("securejoin error: %v", err)
d.Write404(w)
return
}
gr, err := git.Open(path, ref)
if err != nil {
d.Write404(w)
return
}
files, err := gr.FileTree(treePath)
if err != nil {
d.Write500(w)
log.Println(err)
return
}
data := make(map[string]any)
data["name"] = name
data["displayname"] = getDisplayName(name)
data["ref"] = ref
data["parent"] = treePath
data["desc"] = getDescription(path)
data["dotdot"] = filepath.Dir(treePath)
d.listFiles(files, data, w)
return
}
func (d *deps) FileContent(w http.ResponseWriter, r *http.Request) {
var raw bool
if rawParam, err := strconv.ParseBool(r.URL.Query().Get("raw")); err == nil {
raw = rawParam
}
name := r.PathValue("name")
if d.isIgnored(name) {
d.Write404(w)
return
}
treePath := r.PathValue("rest")
ref := r.PathValue("ref")
name = filepath.Clean(name)
path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
if err != nil {
log.Printf("securejoin error: %v", err)
d.Write404(w)
return
}
gr, err := git.Open(path, ref)
if err != nil {
d.Write404(w)
return
}
contents, err := gr.FileContent(treePath)
if err != nil {
d.Write500(w)
return
}
data := make(map[string]any)
data["name"] = name
data["displayname"] = getDisplayName(name)
data["ref"] = ref
data["desc"] = getDescription(path)
data["path"] = treePath
if raw {
d.showRaw(contents, w)
} else {
if d.c.Meta.SyntaxHighlight == "" {
d.showFile(contents, data, w)
} else {
d.showFileWithHighlight(treePath, contents, data, w)
}
}
}
func (d *deps) Archive(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
if d.isIgnored(name) {
d.Write404(w)
return
}
file := r.PathValue("file")
// TODO: extend this to add more files compression (e.g.: xz)
if !strings.HasSuffix(file, ".tar.gz") {
d.Write404(w)
return
}
ref := strings.TrimSuffix(file, ".tar.gz")
// This allows the browser to use a proper name for the file when
// downloading
filename := fmt.Sprintf("%s-%s.tar.gz", name, ref)
setContentDisposition(w, filename)
setGZipMIME(w)
path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
if err != nil {
log.Printf("securejoin error: %v", err)
d.Write404(w)
return
}
gr, err := git.Open(path, ref)
if err != nil {
d.Write404(w)
return
}
gw := gzip.NewWriter(w)
defer gw.Close()
prefix := fmt.Sprintf("%s-%s", name, ref)
err = gr.WriteTar(gw, prefix)
if err != nil {
// once we start writing to the body we can't report error anymore
// so we are only left with printing the error.
log.Println(err)
return
}
err = gw.Flush()
if err != nil {
// once we start writing to the body we can't report error anymore
// so we are only left with printing the error.
log.Println(err)
return
}
}
func (d *deps) Log(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
if d.isIgnored(name) {
d.Write404(w)
return
}
ref := r.PathValue("ref")
path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
if err != nil {
log.Printf("securejoin error: %v", err)
d.Write404(w)
return
}
gr, err := git.Open(path, ref)
if err != nil {
d.Write404(w)
return
}
commits, err := gr.Commits()
if err != nil {
d.Write500(w)
log.Println(err)
return
}
tpath := filepath.Join(d.c.Dirs.Templates, "*")
t := template.Must(template.ParseGlob(tpath))
data := make(map[string]interface{})
data["commits"] = commits
data["meta"] = d.c.Meta
data["name"] = name
data["displayname"] = getDisplayName(name)
data["ref"] = ref
data["desc"] = getDescription(path)
data["log"] = true
if err := t.ExecuteTemplate(w, "log", data); err != nil {
log.Println(err)
return
}
}
func (d *deps) Diff(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
if d.isIgnored(name) {
d.Write404(w)
return
}
ref := r.PathValue("ref")
path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
if err != nil {
log.Printf("securejoin error: %v", err)
d.Write404(w)
return
}
gr, err := git.Open(path, ref)
if err != nil {
d.Write404(w)
return
}
diff, err := gr.Diff()
if err != nil {
d.Write500(w)
log.Println(err)
return
}
tpath := filepath.Join(d.c.Dirs.Templates, "*")
t := template.Must(template.ParseGlob(tpath))
data := make(map[string]interface{})
data["commit"] = diff.Commit
data["stat"] = diff.Stat
data["diff"] = diff.Diff
data["meta"] = d.c.Meta
data["name"] = name
data["displayname"] = getDisplayName(name)
data["ref"] = ref
data["desc"] = getDescription(path)
if err := t.ExecuteTemplate(w, "commit", data); err != nil {
log.Println(err)
return
}
}
func (d *deps) Refs(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
if d.isIgnored(name) {
d.Write404(w)
return
}
path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
if err != nil {
log.Printf("securejoin error: %v", err)
d.Write404(w)
return
}
gr, err := git.Open(path, "")
if err != nil {
d.Write404(w)
return
}
tags, err := gr.Tags()
if err != nil {
// Non-fatal, we *should* have at least one branch to show.
log.Println(err)
}
branches, err := gr.Branches()
if err != nil {
log.Println(err)
d.Write500(w)
return
}
tpath := filepath.Join(d.c.Dirs.Templates, "*")
t := template.Must(template.ParseGlob(tpath))
data := make(map[string]interface{})
data["meta"] = d.c.Meta
data["name"] = name
data["displayname"] = getDisplayName(name)
data["branches"] = branches
data["tags"] = tags
data["desc"] = getDescription(path)
if err := t.ExecuteTemplate(w, "refs", data); err != nil {
log.Println(err)
return
}
}
func (d *deps) ServeStatic(w http.ResponseWriter, r *http.Request) {
f := r.PathValue("file")
f = filepath.Clean(f)
f, err := securejoin.SecureJoin(d.c.Dirs.Static, f)
if err != nil {
d.Write404(w)
return
}
http.ServeFile(w, r, f)
}

View File

@@ -1,151 +0,0 @@
package routes
import (
"bytes"
"html/template"
"io"
"log"
"net/http"
"path/filepath"
"strings"
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
"realy.lol/cmd/legit/git"
)
func (d *deps) Write404(w http.ResponseWriter) {
tpath := filepath.Join(d.c.Dirs.Templates, "*")
t := template.Must(template.ParseGlob(tpath))
w.WriteHeader(404)
if err := t.ExecuteTemplate(w, "404", nil); err != nil {
log.Printf("404 template: %s", err)
}
}
func (d *deps) Write500(w http.ResponseWriter) {
tpath := filepath.Join(d.c.Dirs.Templates, "*")
t := template.Must(template.ParseGlob(tpath))
w.WriteHeader(500)
if err := t.ExecuteTemplate(w, "500", nil); err != nil {
log.Printf("500 template: %s", err)
}
}
func (d *deps) listFiles(files []git.NiceTree, data map[string]any, w http.ResponseWriter) {
tpath := filepath.Join(d.c.Dirs.Templates, "*")
t := template.Must(template.ParseGlob(tpath))
data["files"] = files
data["meta"] = d.c.Meta
if err := t.ExecuteTemplate(w, "tree", data); err != nil {
log.Println(err)
return
}
}
func countLines(r io.Reader) (int, error) {
buf := make([]byte, 32*1024)
bufLen := 0
count := 0
nl := []byte{'\n'}
for {
c, err := r.Read(buf)
if c > 0 {
bufLen += c
}
count += bytes.Count(buf[:c], nl)
switch {
case err == io.EOF:
/* handle last line not having a newline at the end */
if bufLen >= 1 && buf[(bufLen-1)%(32*1024)] != '\n' {
count++
}
return count, nil
case err != nil:
return 0, err
}
}
}
func (d *deps) showFileWithHighlight(name, content string, data map[string]any, w http.ResponseWriter) {
tpath := filepath.Join(d.c.Dirs.Templates, "*")
t := template.Must(template.ParseGlob(tpath))
lexer := lexers.Get(name)
if lexer == nil {
lexer = lexers.Get(".txt")
}
style := styles.Get(d.c.Meta.SyntaxHighlight)
if style == nil {
style = styles.Get("monokailight")
}
formatter := html.New(
html.WithLineNumbers(true),
html.WithLinkableLineNumbers(true, "L"),
)
iterator, err := lexer.Tokenise(nil, content)
if err != nil {
d.Write500(w)
return
}
var code bytes.Buffer
err = formatter.Format(&code, style, iterator)
if err != nil {
d.Write500(w)
return
}
data["content"] = template.HTML(code.String())
data["meta"] = d.c.Meta
data["chroma"] = true
if err := t.ExecuteTemplate(w, "file", data); err != nil {
log.Println(err)
return
}
}
func (d *deps) showFile(content string, data map[string]any, w http.ResponseWriter) {
tpath := filepath.Join(d.c.Dirs.Templates, "*")
t := template.Must(template.ParseGlob(tpath))
lc, err := countLines(strings.NewReader(content))
if err != nil {
// Non-fatal, we'll just skip showing line numbers in the template.
log.Printf("counting lines: %s", err)
}
lines := make([]int, lc)
if lc > 0 {
for i := range lines {
lines[i] = i + 1
}
}
data["linecount"] = lines
data["content"] = content
data["meta"] = d.c.Meta
data["chroma"] = false
if err := t.ExecuteTemplate(w, "file", data); err != nil {
log.Println(err)
return
}
}
func (d *deps) showRaw(content string, w http.ResponseWriter) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(content))
return
}

View File

@@ -1,118 +0,0 @@
package routes
import (
"io/fs"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"realy.lol/cmd/legit/git"
)
func isGoModule(gr *git.GitRepo) bool {
_, err := gr.FileContent("go.mod")
return err == nil
}
func getDisplayName(name string) string {
return strings.TrimSuffix(name, ".git")
}
func getDescription(path string) (desc string) {
db, err := os.ReadFile(filepath.Join(path, "description"))
if err == nil {
desc = string(db)
} else {
desc = ""
}
return
}
func (d *deps) isUnlisted(name string) bool {
for _, i := range d.c.Repo.Unlisted {
if name == i {
return true
}
}
return false
}
func (d *deps) isIgnored(name string) bool {
for _, i := range d.c.Repo.Ignore {
if name == i {
return true
}
}
return false
}
type repoInfo struct {
Git *git.GitRepo
Path string
Category string
}
func (d *deps) getAllRepos() ([]repoInfo, error) {
repos := []repoInfo{}
max := strings.Count(d.c.Repo.ScanPath, string(os.PathSeparator)) + 2
err := filepath.WalkDir(d.c.Repo.ScanPath, func(path string, de fs.DirEntry, err error) error {
if err != nil {
return err
}
if de.IsDir() {
// Check if we've exceeded our recursion depth
if strings.Count(path, string(os.PathSeparator)) > max {
return fs.SkipDir
}
if d.isIgnored(path) {
return fs.SkipDir
}
// A bare repo should always have at least a HEAD file, if it
// doesn't we can continue recursing
if _, err := os.Lstat(filepath.Join(path, "HEAD")); err == nil {
repo, err := git.Open(path, "")
if err != nil {
log.Println(err)
} else {
relpath, _ := filepath.Rel(d.c.Repo.ScanPath, path)
repos = append(repos, repoInfo{
Git: repo,
Path: relpath,
Category: d.category(path),
})
// Since we found a Git repo, we don't want to recurse
// further
return fs.SkipDir
}
}
}
return nil
})
return repos, err
}
func (d *deps) category(path string) string {
return strings.TrimPrefix(filepath.Dir(strings.TrimPrefix(path, d.c.Repo.ScanPath)), string(os.PathSeparator))
}
func setContentDisposition(w http.ResponseWriter, name string) {
h := "inline; filename=\"" + name + "\""
w.Header().Add("Content-Disposition", h)
}
func setGZipMIME(w http.ResponseWriter) {
setMIME(w, "application/gzip")
}
func setMIME(w http.ResponseWriter, mime string) {
w.Header().Add("Content-Type", mime)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,332 +0,0 @@
:root {
--white: #fff;
--light: #f4f4f4;
--cyan: #509c93;
--light-gray: #eee;
--medium-gray: #ddd;
--gray: #6a6a6a;
--dark: #444;
--darker: #222;
--sans-font: -apple-system, BlinkMacSystemFont, "Inter", "Roboto", "Segoe UI", sans-serif;
--display-font: -apple-system, BlinkMacSystemFont, "Inter", "Roboto", "Segoe UI", sans-serif;
--mono-font: 'SF Mono', SFMono-Regular, ui-monospace, 'DejaVu Sans Mono', 'Roboto Mono', Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark light;
--light: #181818;
--cyan: #76c7c0;
--light-gray: #333;
--medium-gray: #444;
--gray: #aaa;
--dark: #ddd;
--darker: #f4f4f4;
--white: #000;
}
}
html {
background: var(--white);
-webkit-text-size-adjust: none;
font-family: var(--sans-font);
font-weight: 380;
}
pre {
font-family: var(--mono-font);
}
::selection {
background: var(--medium-gray);
opacity: 0.3;
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
body {
max-width: 1000px;
padding: 0 13px;
margin: 40px auto;
}
main, footer {
font-size: 1rem;
padding: 0;
line-height: 160%;
}
header h1, h2, h3 {
font-family: var(--display-font);
}
h2 {
font-weight: 400;
}
strong {
font-weight: 500;
}
main h1 {
padding: 10px 0 10px 0;
}
main h2 {
font-size: 18px;
}
main h2, h3 {
padding: 20px 0 15px 0;
}
nav {
padding: 0.4rem 0 1.5rem 0;
}
nav ul {
padding: 0;
margin: 0;
list-style: none;
padding-bottom: 20px;
}
nav ul li {
padding-right: 10px;
display: inline-block;
}
a {
margin: 0;
padding: 0;
box-sizing: border-box;
text-decoration: none;
word-wrap: break-word;
}
a {
color: var(--darker);
border-bottom: 1.5px solid var(--medium-gray);
}
a:hover {
border-bottom: 1.5px solid var(--gray);
}
.index {
padding-top: 2em;
display: grid;
grid-template-columns: 6em 1fr minmax(0, 7em);
grid-row-gap: 0.5em;
min-width: 0;
}
.clone-url {
padding-top: 2rem;
}
.clone-url pre {
color: var(--dark);
white-space: pre-wrap;
}
.desc {
font-weight: normal;
color: var(--gray);
font-style: italic;
}
.tree {
display: grid;
grid-template-columns: 10ch auto 1fr;
grid-row-gap: 0.5em;
grid-column-gap: 1em;
min-width: 0;
}
.log {
display: grid;
grid-template-columns: 20rem minmax(0, 1fr);
grid-row-gap: 0.8em;
grid-column-gap: 8rem;
margin-bottom: 2em;
padding-bottom: 1em;
border-bottom: 1.5px solid var(--medium-gray);
}
.log pre {
white-space: pre-wrap;
}
.mode, .size {
font-family: var(--mono-font);
}
.size {
text-align: right;
}
.readme pre {
white-space: pre-wrap;
overflow-x: auto;
}
.readme {
background: var(--light-gray);
padding: 0.5rem;
}
.readme ul {
padding: revert;
}
.readme img {
max-width: 100%;
}
.diff {
margin: 1rem 0 1rem 0;
padding: 1rem 0 1rem 0;
border-bottom: 1.5px solid var(--medium-gray);
}
.diff pre {
overflow: scroll;
}
.diff-stat {
padding: 1rem 0 1rem 0;
}
.commit-hash, .commit-email {
font-family: var(--mono-font);
}
.commit-email:before {
content: '<';
}
.commit-email:after {
content: '>';
}
.commit {
margin-bottom: 1rem;
}
.commit pre {
padding-bottom: 1rem;
white-space: pre-wrap;
}
.diff-stat ul li {
list-style: none;
padding-left: 0.5em;
}
.diff-add {
color: green;
}
.diff-del {
color: red;
}
.diff-noop {
color: var(--gray);
}
.ref {
font-family: var(--sans-font);
font-size: 14px;
color: var(--gray);
display: inline-block;
padding-top: 0.7em;
}
.refs pre {
white-space: pre-wrap;
padding-bottom: 0.5rem;
}
.refs strong {
padding-right: 1em;
}
.line-numbers {
white-space: pre-line;
-moz-user-select: -moz-none;
-khtml-user-select: none;
-webkit-user-select: none;
-o-user-select: none;
user-select: none;
display: flex;
float: left;
flex-direction: column;
margin-right: 1ch;
}
.file-wrapper {
display: flex;
flex-direction: row;
grid-template-columns: 1rem minmax(0, 1fr);
gap: 1rem;
padding: 0.5rem;
background: var(--light-gray);
overflow-x: auto;
}
.chroma-file-wrapper {
display: flex;
flex-direction: row;
grid-template-columns: 1rem minmax(0, 1fr);
overflow-x: auto;
}
.file-content {
background: var(--light-gray);
overflow-y: hidden;
overflow-x: auto;
}
.diff-type {
color: var(--gray);
}
.commit-info {
color: var(--gray);
padding-bottom: 1.5rem;
font-size: 0.85rem;
}
@media (max-width: 600px) {
.index {
grid-row-gap: 0.8em;
}
.log {
grid-template-columns: 1fr;
grid-row-gap: 0em;
}
.index {
grid-template-columns: 1fr;
grid-row-gap: 0em;
}
.index-name:not(:first-child) {
padding-top: 1.5rem;
}
.commit-info:not(:last-child) {
padding-bottom: 1.5rem;
}
pre {
font-size: 0.8rem;
}
}

View File

@@ -1,13 +0,0 @@
{{ define "404" }}
<html>
<title>404</title>
{{ template "head" . }}
<body>
{{ template "nav" . }}
<main>
<h3>404 &mdash; nothing like that here.</h3>
</main>
</body>
</html>
{{ end }}

View File

@@ -1,13 +0,0 @@
{{ define "500" }}
<html>
<title>500</title>
{{ template "head" . }}
<body>
{{ template "nav" . }}
<main>
<h3>500 &mdash; something broke!</h3>
</main>
</body>
</html>
{{ end }}

View File

@@ -1,104 +0,0 @@
{{ define "commit" }}
<html>
{{ template "head" . }}
{{ template "repoheader" . }}
<body>
{{ template "nav" . }}
<main>
<section class="commit">
<pre>
{{- .commit.Message -}}
</pre>
<div class="commit-info">
{{ .commit.Author.Name }} <a href="mailto:{{ .commit.Author.Email }}" class="commit-email">{{ .commit.Author.Email}}</a>
<div>{{ .commit.Author.When.Format "Mon, 02 Jan 2006 15:04:05 -0700" }}</div>
</div>
<div>
<strong>commit</strong>
<p><a href="/{{ .name }}/commit/{{ .commit.This }}" class="commit-hash">
{{ .commit.This }}
</a>
</p>
</div>
{{ if .commit.Parent }}
<div>
<strong>parent</strong>
<p><a href="/{{ .name }}/commit/{{ .commit.Parent }}" class="commit-hash">
{{ .commit.Parent }}
</a></p>
</div>
{{ end }}
<div class="diff-stat">
<div>
{{ .stat.FilesChanged }} files changed,
{{ .stat.Insertions }} insertions(+),
{{ .stat.Deletions }} deletions(-)
</div>
<div>
<br>
<strong>jump to</strong>
{{ range .diff }}
<ul>
<li><a href="#{{ .Name.New }}">{{ .Name.New }}</a></li>
</ul>
{{ end }}
</div>
</div>
</section>
<section>
{{ $repo := .name }}
{{ $this := .commit.This }}
{{ $parent := .commit.Parent }}
{{ range .diff }}
<div id="{{ .Name.New }}">
<div class="diff">
{{ if .IsNew }}
<span class="diff-type">A</span>
{{ end }}
{{ if .IsDelete }}
<span class="diff-type">D</span>
{{ end }}
{{ if not (or .IsNew .IsDelete) }}
<span class="diff-type">M</span>
{{ end }}
{{ if .Name.Old }}
<a href="/{{ $repo }}/blob/{{ $parent }}/{{ .Name.Old }}">{{ .Name.Old }}</a>
{{ if .Name.New }}
&#8594;
<a href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.New }}">{{ .Name.New }}</a>
{{ end }}
{{ else }}
<a href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.New }}">{{ .Name.New }}</a>
{{- end -}}
{{ if .IsBinary }}
<p>Not showing binary file.</p>
{{ else }}
<pre>
{{- range .TextFragments -}}
<p>{{- .Header -}}</p>
{{- range .Lines -}}
{{- if eq .Op.String "+" -}}
<span class="diff-add">{{ .String }}</span>
{{- end -}}
{{- if eq .Op.String "-" -}}
<span class="diff-del">{{ .String }}</span>
{{- end -}}
{{- if eq .Op.String " " -}}
<span class="diff-noop">{{ .String }}</span>
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
</pre>
</div>
</div>
{{ end }}
</section>
</main>
</body>
</html>
{{ end }}

View File

@@ -1,36 +0,0 @@
{{ define "file" }}
<html>
{{ template "head" . }}
{{ template "repoheader" . }}
<body>
{{ template "nav" . }}
<main>
<p>{{ .path }} (<a style="color: gray" href="?raw=true">view raw</a>)</p>
{{if .chroma }}
<div class="chroma-file-wrapper">
{{ .content }}
</div>
{{else}}
<div class="file-wrapper">
<table>
<tbody><tr>
<td class="line-numbers">
<pre>
{{- range .linecount }}
<a id="L{{ . }}" href="#L{{ . }}">{{ . }}</a>
{{- end -}}
</pre>
</td>
<td class="file-content">
<pre>
{{- .content -}}
</pre>
</td>
</tbody></tr>
</table>
</div>
{{end}}
</main>
</body>
</html>
{{ end }}

View File

@@ -1,32 +0,0 @@
{{ define "head" }}
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/static/style.css" type="text/css">
<link rel="icon" type="image/png" size="32x32" href="/static/legit.png">
{{ if .parent }}
<title>{{ .meta.Title }} &mdash; {{ .name }} ({{ .ref }}): {{ .parent }}/</title>
{{ else if .path }}
<title>{{ .meta.Title }} &mdash; {{ .name }} ({{ .ref }}): {{ .path }}</title>
{{ else if .files }}
<title>{{ .meta.Title }} &mdash; {{ .name }} ({{ .ref }})</title>
{{ else if .commit }}
<title>{{ .meta.Title }} &mdash; {{ .name }}: {{ .commit.This }}</title>
{{ else if .branches }}
<title>{{ .meta.Title }} &mdash; {{ .name }}: refs</title>
{{ else if .commits }}
{{ if .log }}
<title>{{ .meta.Title }} &mdash; {{ .name }}: log</title>
{{ else }}
<title>{{ .meta.Title }} &mdash; {{ .name }}</title>
{{ end }}
{{ else }}
<title>{{ .meta.Title }}</title>
{{ end }}
{{ if and .servername .gomod }}
<meta name="go-import" content="{{ .servername}}/{{ .name }} git https://{{ .servername }}/{{ .name }}">
{{ end }}
<!-- other meta tags here -->
</head>
{{ end }}

View File

@@ -1,21 +0,0 @@
{{ define "index" }}
<html>
{{ template "head" . }}
<header>
<h1>{{ .meta.Title }}</h1>
<h2>{{ .meta.Description }}</h2>
</header>
<body>
<main>
<div class="index">
{{ range .info }}
<div class="index-name"><a href="/{{ .Name }}">{{ .DisplayName }}</a></div>
<div class="desc">{{ .Desc }}</div>
<div>{{ .Idle }}</div>
{{ end }}
</div>
</main>
</body>
</html>
{{ end }}

View File

@@ -1,25 +0,0 @@
{{ define "log" }}
<html>
{{ template "head" . }}
{{ template "repoheader" . }}
<body>
{{ template "nav" . }}
<main>
{{ $repo := .name }}
<div class="log">
{{ range .commits }}
<div>
<div><a href="/{{ $repo }}/commit/{{ .Hash.String }}" class="commit-hash">{{ slice .Hash.String 0 8 }}</a></div>
<pre>{{ .Message }}</pre>
</div>
<div class="commit-info">
{{ .Author.Name }} <a href="mailto:{{ .Author.Email }}" class="commit-email">{{ .Author.Email }}</a>
<div>{{ .Author.When.Format "Mon, 02 Jan 2006 15:04:05 -0700" }}</div>
</div>
{{ end }}
</div>
</main>
</body>
</html>
{{ end }}

View File

@@ -1,14 +0,0 @@
{{ define "nav" }}
<nav>
<ul>
{{ if .name }}
<li><a href="/{{ .name }}">summary</a>
<li><a href="/{{ .name }}/refs">refs</a>
{{ if .ref }}
<li><a href="/{{ .name }}/tree/{{ .ref }}/">tree</a>
<li><a href="/{{ .name }}/log/{{ .ref }}">log</a>
{{ end }}
{{ end }}
</ul>
</nav>
{{ end }}

View File

@@ -1,40 +0,0 @@
{{ define "refs" }}
<html>
{{ template "head" . }}
{{ template "repoheader" . }}
<body>
{{ template "nav" . }}
<main>
{{ $name := .name }}
<h3>branches</h3>
<div class="refs">
{{ range .branches }}
<div>
<strong>{{ .Name.Short }}</strong>
<a href="/{{ $name }}/tree/{{ .Name.Short }}/">browse</a>
<a href="/{{ $name }}/log/{{ .Name.Short }}">log</a>
<a href="/{{ $name }}/archive/{{ .Name.Short }}.tar.gz">tar.gz</a>
</div>
{{ end }}
</div>
{{ if .tags }}
<h3>tags</h3>
<div class="refs">
{{ range .tags }}
<div>
<strong>{{ .Name }}</strong>
<a href="/{{ $name }}/tree/{{ .Name }}/">browse</a>
<a href="/{{ $name }}/log/{{ .Name }}">log</a>
<a href="/{{ $name }}/archive/{{ .Name }}.tar.gz">tar.gz</a>
{{ if .Message }}
<pre>{{ .Message }}</pre>
</div>
{{ end }}
{{ end }}
</div>
{{ end }}
</main>
</body>
</html>
{{ end }}

View File

@@ -1,12 +0,0 @@
{{ define "repoheader" }}
<header>
<h2>
<a href="/">all repos</a>
&mdash; {{ .displayname }}
{{ if .ref }}
<span class="ref">@ {{ .ref }}</span>
{{ end }}
</h2>
<h3 class="desc">{{ .desc }}</h3>
</header>
{{ end }}

View File

@@ -1,38 +0,0 @@
{{ define "repo" }}
<html>
{{ template "head" . }}
{{ template "repoheader" . }}
<body>
{{ template "nav" . }}
<main>
{{ $repo := .name }}
<div class="log">
{{ range .commits }}
<div>
<div><a href="/{{ $repo }}/commit/{{ .Hash.String }}" class="commit-hash">{{ slice .Hash.String 0 8 }}</a></div>
<pre>{{ .Message }}</pre>
</div>
<div class="commit-info">
{{ .Author.Name }} <a href="mailto:{{ .Author.Email }}" class="commit-email">{{ .Author.Email }}</a>
<div>{{ .Author.When.Format "Mon, 02 Jan 2006 15:04:05 -0700" }}</div>
</div>
{{ end }}
</div>
{{- if .readme }}
<article class="readme">
{{- .readme -}}
</article>
{{- end -}}
<div class="clone-url">
<strong>clone</strong>
<pre>
git clone https://{{ .servername }}/{{ .name }}
</pre>
</div>
</main>
</body>
</html>
{{ end }}

View File

@@ -1,55 +0,0 @@
{{ define "tree" }}
<html>
{{ template "head" . }}
{{ template "repoheader" . }}
<body>
{{ template "nav" . }}
<main>
{{ $repo := .name }}
{{ $ref := .ref }}
{{ $parent := .parent }}
<div class="tree">
{{ if $parent }}
<div></div>
<div></div>
<div><a href="/{{ $repo }}/tree/{{ $ref }}/{{ .dotdot }}">..</a></div>
{{ end }}
{{ range .files }}
{{ if not .IsFile }}
<div class="mode">{{ .Mode }}</div>
<div class="size">{{ .Size }}</div>
<div>
{{ if $parent }}
<a href="/{{ $repo }}/tree/{{ $ref }}/{{ $parent }}/{{ .Name }}">{{ .Name }}/</a>
{{ else }}
<a href="/{{ $repo }}/tree/{{ $ref }}/{{ .Name }}">{{ .Name }}/</a>
{{ end }}
</div>
{{ end }}
{{ end }}
{{ range .files }}
{{ if .IsFile }}
<div class="mode">{{ .Mode }}</div>
<div class="size">{{ .Size }}</div>
<div>
{{ if $parent }}
<a href="/{{ $repo }}/blob/{{ $ref }}/{{ $parent }}/{{ .Name }}">{{ .Name }}</a>
{{ else }}
<a href="/{{ $repo }}/blob/{{ $ref }}/{{ .Name }}">{{ .Name }}</a>
{{ end }}
</div>
{{ end }}
{{ end }}
</div>
<article>
<pre>
{{- if .readme }}{{ .readme }}{{- end -}}
</pre>
</article>
</main>
</body>
</html>
{{ end }}

View File

@@ -1,29 +0,0 @@
//go:build openbsd
// +build openbsd
package main
import (
"log"
"golang.org/x/sys/unix"
)
func Unveil(path string, perms string) error {
log.Printf("unveil: \"%s\", %s", path, perms)
return unix.Unveil(path, perms)
}
func UnveilBlock() error {
log.Printf("unveil: block")
return unix.UnveilBlock()
}
func UnveilPaths(paths []string, perms string) error {
for _, path := range paths {
if err := Unveil(path, perms); err != nil {
return err
}
}
return UnveilBlock()
}

View File

@@ -1,18 +0,0 @@
//go:build !openbsd
// +build !openbsd
// Stub functions for GOOS that don't support unix.Unveil()
package main
func Unveil(path string, perms string) error {
return nil
}
func UnveilBlock() error {
return nil
}
func UnveilPaths(paths []string, perms string) error {
return nil
}

1
go.mod
View File

@@ -46,6 +46,7 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rickb777/acceptable v0.46.0 // indirect
github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 // indirect
github.com/templexxx/cpu v0.1.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect

2
go.sum
View File

@@ -87,6 +87,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/rickb777/acceptable v0.46.0 h1:G/GoAKyQlDKKmEl9kbp9mgkf4vHvcxB6Vx/l2MHEdJ8=
github.com/rickb777/acceptable v0.46.0/go.mod h1:MHPJml//8ogaImxiVu6qmOlpoFiKtuLOaTEGSl+Mmos=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=

View File

@@ -16,8 +16,9 @@ import (
// ConfigurationSetInput is the parameters for HTTP API method to set Configuration.
type ConfigurationSetInput struct {
Auth string `header:"Authorization" doc:"nostr nip-98 (and expiring variant)" required:"true"`
Body config.C `doc:"the new configuration"`
Auth string `header:"Authorization" doc:"nostr nip-98 (and expiring variant)" required:"true"`
Body config.C `doc:"the new configuration"`
Accept string `header:"Accept" default:"application/json" required:"false"`
}
// ConfigurationGetInput is the parameters for HTTP API method to get Configuration.

View File

@@ -25,6 +25,7 @@ import (
// EventInput is the parameters for the Event HTTP API method.
type EventInput struct {
Auth string `header:"Authorization" doc:"nostr nip-98 (and expiring variant)" required:"false"`
Accept string `header:"Accept" default:"application/nostr+json;q=0.9,application/x-realy-event:q=0.1"`
RawBody []byte
}

View File

@@ -6,6 +6,7 @@ import (
"net/http"
"github.com/danielgtaylor/huma/v2"
"github.com/rickb777/acceptable/header"
"realy.lol/chk"
"realy.lol/context"
@@ -20,8 +21,13 @@ import (
// EventsInput is the parameters for an Events HTTP API method. Basically an array of eventid.T.
type EventsInput struct {
Auth string `header:"Authorization" doc:"nostr nip-98 (and expiring variant)" required:"false"`
Body []string `doc:"list of event Ids"`
Auth string `header:"Authorization" doc:"nostr nip-98 (and expiring variant)" required:"false"`
Accept string `header:"Accept" default:"application/nostr+json;q=0.9,application/x-realy-event;q=0.1" doc:"event encoding format that is expected, priority using mimetype;q=0.x will indicate preference when multiple are available"`
Body []string `doc:"list of event Ids"`
}
type EventsOutput struct {
Limit int `header:"X-Limit" default:"1000" doc:"informs client maximum number of events that they can request"`
}
// RegisterEvents is the implementation of the HTTP API for Events.
@@ -32,15 +38,14 @@ func (x *Operations) RegisterEvents(api huma.API) {
scopes := []string{"user", "read"}
method := http.MethodPost
huma.Register(api, huma.Operation{
OperationID: name,
Summary: name,
Path: path,
Method: method,
Tags: []string{"events"},
Description: helpers.GenerateDescription(description, scopes),
Security: []map[string][]string{{"auth": scopes}},
DefaultStatus: 204,
}, func(ctx context.T, input *EventsInput) (output *huma.StreamResponse, err error) {
OperationID: name,
Summary: name,
Path: path,
Method: method,
Tags: []string{"events"},
Description: helpers.GenerateDescription(description, scopes),
Security: []map[string][]string{{"auth": scopes}},
}, func(ctx context.T, input *EventsInput) (output *struct{}, err error) {
// log.I.S(input)
if len(input.Body) == 10000 {
err = huma.Error400BadRequest(
@@ -52,6 +57,10 @@ func (x *Operations) RegisterEvents(api huma.API) {
if len(input.Body) > 1000 || x.Server.AuthRequired() {
authrequired = true
}
limit := 1000
if !authrequired {
limit = 10000
}
r := ctx.Value("http-request").(*http.Request)
var valid bool
var pubkey []byte
@@ -97,14 +106,27 @@ func (x *Operations) RegisterEvents(api huma.API) {
}
evIds = append(evIds, idb)
}
if idsWriter, ok := sto.(store.GetIdsWriter); ok {
output = &huma.StreamResponse{
func(ctx huma.Context) {
if err = idsWriter.FetchIds(x.Context(), tag.New(evIds...),
ctx.BodyWriter()); chk.E(err) {
return
}
},
w := ctx.Value("http-response").(http.ResponseWriter)
var binary bool
precedence := header.ParsePrecedenceValues(r.Header.Get("Accept"))
done:
for _, v := range precedence {
switch v.Value {
case "application/x-realy-event":
binary = true
break done
case "application/nostr+json":
break done
default:
break done
}
}
w.WriteHeader(200)
w.Header().Set("X-Limit", fmt.Sprint(limit))
if err = idsWriter.FetchIds(w, x.Context(), tag.New(evIds...), binary); chk.E(err) {
return
}
}
return

View File

@@ -1,12 +1,14 @@
package ratel
import (
"bytes"
"io"
"github.com/dgraph-io/badger/v4"
"realy.lol/chk"
"realy.lol/context"
"realy.lol/event"
"realy.lol/ratel/keys/id"
"realy.lol/ratel/keys/serial"
"realy.lol/ratel/prefixes"
@@ -14,7 +16,7 @@ import (
)
// FetchIds retrieves events based on a list of event Ids that have been provided.
func (r *T) FetchIds(c context.T, evIds *tag.T, out io.Writer) (err error) {
func (r *T) FetchIds(w io.Writer, c context.T, evIds *tag.T, binary bool) (err error) {
b := make([]byte, 0, 100000)
err = r.View(func(txn *badger.Txn) (err error) {
for _, v := range evIds.ToSliceOfBytes() {
@@ -38,11 +40,30 @@ func (r *T) FetchIds(c context.T, evIds *tag.T, out io.Writer) (err error) {
if b, err = item.ValueCopy(nil); chk.E(err) {
return
}
if _, err = out.Write(b); chk.E(err) {
if binary {
if !r.Binary {
ev := event.New()
if b, err = ev.Unmarshal(b); chk.E(err) {
return
}
ev.MarshalBinary(w)
continue
}
} else {
if r.Binary {
ev := event.New()
buf := bytes.NewBuffer(b)
if err = ev.UnmarshalBinary(buf); chk.E(err) {
return
}
b = ev.Marshal(nil)
}
}
if _, err = w.Write(b); chk.E(err) {
return
}
// add the new line after entries
if _, err = out.Write([]byte{'\n'}); chk.E(err) {
if _, err = w.Write([]byte{'\n'}); chk.E(err) {
return
}
}

View File

@@ -72,7 +72,7 @@ type Querier interface {
}
type GetIdsWriter interface {
FetchIds(c context.T, evIds *tag.T, out io.Writer) (err error)
FetchIds(w io.Writer, c context.T, evIds *tag.T, binary bool) (err error)
}
type Deleter interface {

View File

@@ -1 +1 @@
v1.17.0
v1.18.0