forked from tangled.org/core
this repo has no description

delete outdated legit code

-122
auth/auth.go
···
-
package auth
-
-
import (
-
"context"
-
"fmt"
-
"net/http"
-
"time"
-
-
comatproto "github.com/bluesky-social/indigo/api/atproto"
-
"github.com/bluesky-social/indigo/atproto/identity"
-
"github.com/bluesky-social/indigo/atproto/syntax"
-
"github.com/bluesky-social/indigo/xrpc"
-
"github.com/gorilla/sessions"
-
)
-
-
type Auth struct {
-
s sessions.Store
-
}
-
-
func NewAuth(store sessions.Store) *Auth {
-
return &Auth{store}
-
}
-
-
func ResolveIdent(ctx context.Context, arg string) (*identity.Identity, error) {
-
id, err := syntax.ParseAtIdentifier(arg)
-
if err != nil {
-
return nil, err
-
}
-
-
dir := identity.DefaultDirectory()
-
return dir.Lookup(ctx, *id)
-
}
-
-
func (a *Auth) AuthorizedClient(r *http.Request) (*xrpc.Client, error) {
-
clientSession, err := a.s.Get(r, "bild-session")
-
-
if err != nil || clientSession.IsNew {
-
return nil, err
-
}
-
-
did := clientSession.Values["did"].(string)
-
pdsUrl := clientSession.Values["pds"].(string)
-
accessJwt := clientSession.Values["accessJwt"].(string)
-
refreshJwt := clientSession.Values["refreshJwt"].(string)
-
-
client := &xrpc.Client{
-
Host: pdsUrl,
-
Auth: &xrpc.AuthInfo{
-
AccessJwt: accessJwt,
-
RefreshJwt: refreshJwt,
-
Did: did,
-
},
-
}
-
-
return client, nil
-
}
-
-
func (a *Auth) CreateInitialSession(w http.ResponseWriter, r *http.Request, username, appPassword string) (AtSessionCreate, error) {
-
ctx := r.Context()
-
resolved, err := ResolveIdent(ctx, username)
-
if err != nil {
-
return AtSessionCreate{}, fmt.Errorf("invalid handle: %s", err)
-
}
-
-
pdsUrl := resolved.PDSEndpoint()
-
client := xrpc.Client{
-
Host: pdsUrl,
-
}
-
-
atSession, err := comatproto.ServerCreateSession(ctx, &client, &comatproto.ServerCreateSession_Input{
-
Identifier: resolved.DID.String(),
-
Password: appPassword,
-
})
-
if err != nil {
-
return AtSessionCreate{}, fmt.Errorf("invalid app password")
-
}
-
-
return AtSessionCreate{
-
ServerCreateSession_Output: *atSession,
-
PDSEndpoint: pdsUrl,
-
}, nil
-
}
-
-
func (a *Auth) StoreSession(r *http.Request, w http.ResponseWriter, atSessionCreate *AtSessionCreate, atSessionRefresh *AtSessionRefresh) error {
-
if atSessionCreate != nil {
-
atSession := atSessionCreate
-
-
clientSession, _ := a.s.Get(r, "bild-session")
-
clientSession.Values["handle"] = atSession.Handle
-
clientSession.Values["did"] = atSession.Did
-
clientSession.Values["accessJwt"] = atSession.AccessJwt
-
clientSession.Values["refreshJwt"] = atSession.RefreshJwt
-
clientSession.Values["expiry"] = time.Now().Add(time.Hour).String()
-
clientSession.Values["pds"] = atSession.PDSEndpoint
-
clientSession.Values["authenticated"] = true
-
-
return clientSession.Save(r, w)
-
} else {
-
atSession := atSessionRefresh
-
-
clientSession, _ := a.s.Get(r, "bild-session")
-
clientSession.Values["handle"] = atSession.Handle
-
clientSession.Values["did"] = atSession.Did
-
clientSession.Values["accessJwt"] = atSession.AccessJwt
-
clientSession.Values["refreshJwt"] = atSession.RefreshJwt
-
clientSession.Values["expiry"] = time.Now().Add(time.Hour).String()
-
clientSession.Values["pds"] = atSession.PDSEndpoint
-
clientSession.Values["authenticated"] = true
-
-
return clientSession.Save(r, w)
-
}
-
}
-
-
func (a *Auth) GetSessionUser(r *http.Request) (*identity.Identity, error) {
-
session, _ := a.s.Get(r, "bild-session")
-
did, ok := session.Values["did"].(string)
-
if !ok {
-
return nil, fmt.Errorf("user is not authenticated")
-
}
-
-
return ResolveIdent(r.Context(), did)
-
}
-15
auth/types.go
···
-
package auth
-
-
import (
-
comatproto "github.com/bluesky-social/indigo/api/atproto"
-
)
-
-
type AtSessionCreate struct {
-
comatproto.ServerCreateSession_Output
-
PDSEndpoint string
-
}
-
-
type AtSessionRefresh struct {
-
comatproto.ServerRefreshSession_Output
-
PDSEndpoint string
-
}
-47
cmd/legit/main.go
···
-
package main
-
-
import (
-
"flag"
-
"fmt"
-
"log"
-
"log/slog"
-
"net/http"
-
"os"
-
-
"github.com/icyphox/bild/config"
-
"github.com/icyphox/bild/db"
-
"github.com/icyphox/bild/routes"
-
)
-
-
func main() {
-
var cfg string
-
flag.StringVar(&cfg, "config", "./config.yaml", "path to config file")
-
flag.Parse()
-
-
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, nil)))
-
-
c, err := config.Read(cfg)
-
if err != nil {
-
log.Fatal(err)
-
}
-
db, err := db.Setup(c.Server.DBPath)
-
if err != nil {
-
log.Fatalf("failed to setup db: %s", err)
-
}
-
-
mux, err := routes.Setup(c, db)
-
if err != nil {
-
log.Fatal(err)
-
}
-
-
internalMux := routes.SetupInternal(c, db)
-
-
addr := fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port)
-
internalAddr := fmt.Sprintf("%s:%d", c.Server.InternalHost, c.Server.InternalPort)
-
-
log.Println("starting main server on", addr)
-
go http.ListenAndServe(addr, mux)
-
-
log.Println("starting internal server on", internalAddr)
-
log.Fatal(http.ListenAndServe(internalAddr, internalMux))
-
}
-61
config/config.go
···
-
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"`
-
DBPath string `yaml:"dbpath"`
-
-
InternalHost string `yaml:"internalHost,omitempty"`
-
InternalPort int `yaml:"internalPort,omitempty"`
-
} `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
-
}
-22
contrib/Dockerfile
···
-
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"]
-14
contrib/docker-compose.yml
···
-
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
-17
contrib/legit.service
···
-
[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
-80
db/access.go
···
-
package db
-
-
import (
-
"log"
-
"strings"
-
)
-
-
// forms a poset
-
type Level int
-
-
const (
-
Reader Level = iota
-
Writer
-
Owner
-
)
-
-
var (
-
levelMap = map[string]Level{
-
"writer": Writer,
-
"owner": Owner,
-
}
-
)
-
-
func ParseLevel(str string) (Level, bool) {
-
c, ok := levelMap[strings.ToLower(str)]
-
return c, ok
-
}
-
-
func (l Level) String() string {
-
switch l {
-
case Owner:
-
return "OWNER"
-
case Writer:
-
return "WRITER"
-
case Reader:
-
return "READER"
-
default:
-
return "READER"
-
}
-
}
-
-
func (d *DB) SetAccessLevel(userDid string, repoDid string, repoName string, level Level) error {
-
_, err := d.db.Exec(
-
`insert
-
into access_levels (repo_id, did, access)
-
values ((select id from repos where did = $1 and name = $2), $3, $4)
-
on conflict (repo_id, did)
-
do update set access = $4;`,
-
repoDid, repoName, userDid, level.String())
-
return err
-
}
-
-
func (d *DB) SetOwner(userDid string, repoDid string, repoName string) error {
-
return d.SetAccessLevel(userDid, repoDid, repoName, Owner)
-
}
-
-
func (d *DB) SetWriter(userDid string, repoDid string, repoName string) error {
-
return d.SetAccessLevel(userDid, repoDid, repoName, Writer)
-
}
-
-
func (d *DB) GetAccessLevel(userDid string, repoDid string, repoName string) (Level, error) {
-
row := d.db.QueryRow(`
-
select access_levels.access
-
from repos
-
join access_levels
-
on repos.id = access_levels.repo_id
-
where access_levels.did = ? and repos.did = ? and repos.name = ?
-
`, userDid, repoDid, repoName)
-
-
var levelStr string
-
err := row.Scan(&levelStr)
-
if err != nil {
-
log.Println(err)
-
return Reader, err
-
} else {
-
level, _ := ParseLevel(levelStr)
-
return level, nil
-
}
-
-
}
-51
db/init.go
···
-
package db
-
-
import (
-
"database/sql"
-
-
_ "github.com/mattn/go-sqlite3"
-
)
-
-
type DB struct {
-
db *sql.DB
-
}
-
-
func Setup(dbPath string) (*DB, error) {
-
db, err := sql.Open("sqlite3", dbPath)
-
if err != nil {
-
return nil, err
-
}
-
-
_, err = db.Exec(`
-
create table if not exists public_keys (
-
id integer primary key autoincrement,
-
did text not null,
-
name text not null,
-
key text not null,
-
created timestamp default current_timestamp,
-
unique(did, name, key)
-
);
-
create table if not exists repos (
-
id integer primary key autoincrement,
-
did text not null,
-
name text not null,
-
description text not null,
-
created timestamp default current_timestamp,
-
unique(did, name)
-
);
-
create table if not exists access_levels (
-
id integer primary key autoincrement,
-
repo_id integer not null,
-
did text not null,
-
access text not null check (access in ('OWNER', 'WRITER')),
-
created timestamp default current_timestamp,
-
unique(repo_id, did),
-
foreign key (repo_id) references repos(id) on delete cascade
-
);
-
`)
-
if err != nil {
-
return nil, err
-
}
-
-
return &DB{db: db}, nil
-
}
-70
db/pubkeys.go
···
-
package db
-
-
import "time"
-
-
func (d *DB) AddPublicKey(did, name, key string) error {
-
query := `insert into public_keys (did, name, key) values (?, ?, ?)`
-
_, err := d.db.Exec(query, did, name, key)
-
return err
-
}
-
-
func (d *DB) RemovePublicKey(did string) error {
-
query := `delete from public_keys where did = ?`
-
_, err := d.db.Exec(query, did)
-
return err
-
}
-
-
type PublicKey struct {
-
Key string
-
Name string
-
DID string
-
Created time.Time
-
}
-
-
func (d *DB) GetAllPublicKeys() ([]PublicKey, error) {
-
var keys []PublicKey
-
-
rows, err := d.db.Query(`select key, name, did, created from public_keys`)
-
if err != nil {
-
return nil, err
-
}
-
defer rows.Close()
-
-
for rows.Next() {
-
var publicKey PublicKey
-
if err := rows.Scan(&publicKey.Key, &publicKey.Name, &publicKey.DID, &publicKey.Created); err != nil {
-
return nil, err
-
}
-
keys = append(keys, publicKey)
-
}
-
-
if err := rows.Err(); err != nil {
-
return nil, err
-
}
-
-
return keys, nil
-
}
-
-
func (d *DB) GetPublicKeys(did string) ([]PublicKey, error) {
-
var keys []PublicKey
-
-
rows, err := d.db.Query(`select did, key, name, created from public_keys where did = ?`, did)
-
if err != nil {
-
return nil, err
-
}
-
defer rows.Close()
-
-
for rows.Next() {
-
var publicKey PublicKey
-
if err := rows.Scan(&publicKey.DID, &publicKey.Key, &publicKey.Name, &publicKey.Created); err != nil {
-
return nil, err
-
}
-
keys = append(keys, publicKey)
-
}
-
-
if err := rows.Err(); err != nil {
-
return nil, err
-
}
-
-
return keys, nil
-
}
-25
db/repo.go
···
-
package db
-
-
func (d *DB) AddRepo(did string, name string, description string) error {
-
_, err := d.db.Exec("insert into repos (did, name, description) values (?, ?, ?)", did, name, description)
-
if err != nil {
-
return err
-
}
-
return nil
-
}
-
-
func (d *DB) RemoveRepo(did string) error {
-
_, err := d.db.Exec("delete from repos where did = ?", did)
-
if err != nil {
-
return err
-
}
-
return nil
-
}
-
-
func (d *DB) UpdateRepo(did string, name string, description string) error {
-
_, err := d.db.Exec("update repos set name = ?, description = ? where did = ?", name, description, did)
-
if err != nil {
-
return err
-
}
-
return nil
-
}
-119
git/diff.go
···
-
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
-
}
-345
git/git.go
···
-
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 ""
-
}
-33
git/repo.go
···
-
package git
-
-
import (
-
"errors"
-
"fmt"
-
"os"
-
"path/filepath"
-
-
gogit "github.com/go-git/go-git/v5"
-
"github.com/go-git/go-git/v5/config"
-
)
-
-
func InitBare(path string) error {
-
parent := filepath.Dir(path)
-
-
if err := os.MkdirAll(parent, 0755); errors.Is(err, os.ErrExist) {
-
return fmt.Errorf("error creating user directory: %w", err)
-
}
-
-
repository, err := gogit.PlainInit(path, true)
-
if err != nil {
-
return err
-
}
-
-
err = repository.CreateBranch(&config.Branch{
-
Name: "main",
-
})
-
if err != nil {
-
return fmt.Errorf("creating branch: %w", err)
-
}
-
-
return nil
-
}
-121
git/service/service.go
···
-
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
-
}
-25
git/service/write_flusher.go
···
-
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)
-
}
-66
git/tree.go
···
-
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
-
}
+1 -1
knotserver/routes.go
···
"github.com/go-chi/chi/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
-
"github.com/icyphox/bild/db"
+
"github.com/icyphox/bild/knotserver/db"
"github.com/icyphox/bild/knotserver/git"
"github.com/russross/blackfriday/v2"
)
-35
routes/access.go
···
-
package routes
-
-
import (
-
"github.com/go-chi/chi/v5"
-
"github.com/icyphox/bild/auth"
-
"github.com/icyphox/bild/db"
-
"log"
-
"net/http"
-
)
-
-
func (h *Handle) AccessLevel(level db.Level) func(http.Handler) http.Handler {
-
return func(next http.Handler) http.Handler {
-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-
repoOwnerHandle := chi.URLParam(r, "user")
-
repoOwner, err := auth.ResolveIdent(r.Context(), repoOwnerHandle)
-
if err != nil {
-
log.Println("invalid did")
-
http.Error(w, "invalid did", http.StatusNotFound)
-
return
-
}
-
repoName := chi.URLParam(r, "name")
-
session, _ := h.s.Get(r, "bild-session")
-
did := session.Values["did"].(string)
-
-
userLevel, err := h.db.GetAccessLevel(did, repoOwner.DID.String(), repoName)
-
if err != nil || userLevel < level {
-
log.Printf("unauthorized access: %s accessing %s/%s\n", did, repoOwnerHandle, repoName)
-
log.Printf("wanted level: %s, got level %s", level.String(), userLevel.String())
-
http.Error(w, "Forbidden", http.StatusUnauthorized)
-
return
-
}
-
next.ServeHTTP(w, r)
-
})
-
}
-
}
-72
routes/auth.go
···
-
package routes
-
-
import (
-
"log"
-
"net/http"
-
"time"
-
-
comatproto "github.com/bluesky-social/indigo/api/atproto"
-
"github.com/bluesky-social/indigo/xrpc"
-
rauth "github.com/icyphox/bild/auth"
-
)
-
-
const (
-
layout = "2006-01-02 15:04:05.999999999 -0700 MST"
-
)
-
-
func (h *Handle) AuthMiddleware(next http.Handler) http.Handler {
-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-
session, _ := h.s.Get(r, "bild-session")
-
auth, ok := session.Values["authenticated"].(bool)
-
-
if !ok || !auth {
-
log.Printf("not logged in, redirecting")
-
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
-
return
-
}
-
-
// refresh if nearing expiry
-
// TODO: dedup with /login
-
expiryStr := session.Values["expiry"].(string)
-
expiry, _ := time.Parse(layout, expiryStr)
-
pdsUrl := session.Values["pds"].(string)
-
did := session.Values["did"].(string)
-
refreshJwt := session.Values["refreshJwt"].(string)
-
-
if time.Now().After((expiry)) {
-
log.Println("token expired, refreshing ...")
-
-
client := xrpc.Client{
-
Host: pdsUrl,
-
Auth: &xrpc.AuthInfo{
-
Did: did,
-
AccessJwt: refreshJwt,
-
RefreshJwt: refreshJwt,
-
},
-
}
-
atSession, err := comatproto.ServerRefreshSession(r.Context(), &client)
-
if err != nil {
-
log.Println(err)
-
h.Write500(w)
-
return
-
}
-
-
err = h.auth.StoreSession(r, w, nil, &rauth.AtSessionRefresh{ServerRefreshSession_Output: *atSession, PDSEndpoint: pdsUrl})
-
if err != nil {
-
log.Printf("failed to store session for did: %s\n: %s", atSession.Did, err)
-
h.Write500(w)
-
return
-
}
-
-
log.Println("successfully refreshed token")
-
}
-
-
if r.URL.Path == "/login" {
-
log.Println("already logged in")
-
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
-
return
-
}
-
-
next.ServeHTTP(w, r)
-
})
-
}
-122
routes/file.go
···
-
package routes
-
-
import (
-
"bytes"
-
"html/template"
-
"io"
-
"log"
-
"net/http"
-
"strings"
-
-
"github.com/alecthomas/chroma/v2/formatters/html"
-
"github.com/alecthomas/chroma/v2/lexers"
-
"github.com/alecthomas/chroma/v2/styles"
-
"github.com/icyphox/bild/git"
-
)
-
-
func (h *Handle) listFiles(files []git.NiceTree, data map[string]any, w http.ResponseWriter) {
-
data["files"] = files
-
data["meta"] = h.c.Meta
-
-
if err := h.t.ExecuteTemplate(w, "repo/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 (h *Handle) showFileWithHighlight(name, content string, data map[string]any, w http.ResponseWriter) {
-
lexer := lexers.Get(name)
-
if lexer == nil {
-
lexer = lexers.Get(".txt")
-
}
-
-
style := styles.Get(h.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 {
-
h.Write500(w)
-
return
-
}
-
-
var code bytes.Buffer
-
err = formatter.Format(&code, style, iterator)
-
if err != nil {
-
h.Write500(w)
-
return
-
}
-
-
data["content"] = template.HTML(code.String())
-
data["meta"] = h.c.Meta
-
data["chroma"] = true
-
-
if err := h.t.ExecuteTemplate(w, "repo/file", data); err != nil {
-
log.Println(err)
-
return
-
}
-
}
-
-
func (h *Handle) showFile(content string, data map[string]any, w http.ResponseWriter) {
-
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"] = h.c.Meta
-
data["chroma"] = false
-
-
if err := h.t.ExecuteTemplate(w, "repo/file", data); err != nil {
-
log.Println(err)
-
return
-
}
-
}
-
-
func (h *Handle) showRaw(content string, w http.ResponseWriter) {
-
w.WriteHeader(http.StatusOK)
-
w.Header().Set("Content-Type", "text/plain")
-
w.Write([]byte(content))
-
return
-
}
-69
routes/git.go
···
-
package routes
-
-
import (
-
"compress/gzip"
-
"io"
-
"log"
-
"net/http"
-
"path/filepath"
-
-
"github.com/icyphox/bild/git/service"
-
)
-
-
func (d *Handle) InfoRefs(w http.ResponseWriter, r *http.Request) {
-
name := displayRepoName(r)
-
name = filepath.Clean(name)
-
-
repo := filepath.Join(d.c.Repo.ScanPath, name)
-
-
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 *Handle) UploadPack(w http.ResponseWriter, r *http.Request) {
-
name := displayRepoName(r)
-
name = filepath.Clean(name)
-
-
repo := filepath.Join(d.c.Repo.ScanPath, name)
-
-
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
-
}
-
}
-118
routes/handler.go
···
-
package routes
-
-
import (
-
"fmt"
-
"net/http"
-
-
_ "github.com/bluesky-social/indigo/atproto/identity"
-
_ "github.com/bluesky-social/indigo/atproto/syntax"
-
_ "github.com/bluesky-social/indigo/xrpc"
-
"github.com/go-chi/chi/v5"
-
"github.com/gorilla/sessions"
-
"github.com/icyphox/bild/auth"
-
"github.com/icyphox/bild/config"
-
database "github.com/icyphox/bild/db"
-
"github.com/icyphox/bild/routes/middleware"
-
"github.com/icyphox/bild/routes/tmpl"
-
)
-
-
// Checks for gitprotocol-http(5) specific smells; if found, passes
-
// the request on to the git http service, else render the web frontend.
-
func (h *Handle) Multiplex(w http.ResponseWriter, r *http.Request) {
-
path := chi.URLParam(r, "*")
-
-
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" {
-
h.InfoRefs(w, r)
-
} else if path == "git-upload-pack" && r.Method == "POST" {
-
h.UploadPack(w, r)
-
} else if r.Method == "GET" {
-
h.RepoIndex(w, r)
-
}
-
}
-
-
func Setup(c *config.Config, db *database.DB) (http.Handler, error) {
-
r := chi.NewRouter()
-
s := sessions.NewCookieStore([]byte("TODO_CHANGE_ME"))
-
t, err := tmpl.Load(c.Dirs.Templates)
-
if err != nil {
-
return nil, fmt.Errorf("failed to load templates: %w", err)
-
}
-
-
auth := auth.NewAuth(s)
-
-
h := Handle{
-
c: c,
-
t: t,
-
s: s,
-
db: db,
-
auth: auth,
-
}
-
-
r.Get("/", h.Timeline)
-
-
r.Group(func(r chi.Router) {
-
r.Get("/login", h.Login)
-
r.Post("/login", h.Login)
-
})
-
r.Get("/static/{file}", h.ServeStatic)
-
-
r.Route("/repo", func(r chi.Router) {
-
r.Use(h.AuthMiddleware)
-
r.Get("/new", h.NewRepo)
-
r.Put("/new", h.NewRepo)
-
})
-
-
r.Group(func(r chi.Router) {
-
r.Use(h.AuthMiddleware)
-
r.Route("/settings", func(r chi.Router) {
-
r.Get("/keys", h.Keys)
-
r.Put("/keys", h.Keys)
-
})
-
})
-
-
r.Route("/@{user}", func(r chi.Router) {
-
r.Use(middleware.AddDID)
-
r.Get("/", h.Index)
-
-
// Repo routes
-
r.Route("/{name}", func(r chi.Router) {
-
r.Get("/", h.Multiplex)
-
r.Post("/", h.Multiplex)
-
-
r.Route("/tree/{ref}", func(r chi.Router) {
-
r.Get("/*", h.RepoTree)
-
})
-
-
r.Route("/blob/{ref}", func(r chi.Router) {
-
r.Get("/*", h.FileContent)
-
})
-
-
r.Get("/log/{ref}", h.Log)
-
r.Get("/archive/{file}", h.Archive)
-
r.Get("/commit/{ref}", h.Diff)
-
r.Get("/refs/", h.Refs)
-
-
r.Group(func(r chi.Router) {
-
// settings page is only accessible to owners
-
r.Use(h.AccessLevel(database.Owner))
-
r.Route("/settings", func(r chi.Router) {
-
r.Put("/collaborators", h.Collaborators)
-
})
-
})
-
-
// Catch-all routes
-
r.Get("/*", h.Multiplex)
-
r.Post("/*", h.Multiplex)
-
})
-
})
-
-
return r, nil
-
}
-29
routes/html_util.go
···
-
package routes
-
-
import (
-
"fmt"
-
"log"
-
"net/http"
-
)
-
-
func (h *Handle) Write404(w http.ResponseWriter) {
-
w.WriteHeader(404)
-
if err := h.t.ExecuteTemplate(w, "errors/404", nil); err != nil {
-
log.Printf("404 template: %s", err)
-
}
-
}
-
-
func (h *Handle) Write500(w http.ResponseWriter) {
-
w.WriteHeader(500)
-
if err := h.t.ExecuteTemplate(w, "errors/500", nil); err != nil {
-
log.Printf("500 template: %s", err)
-
}
-
}
-
-
func (h *Handle) WriteOOBNotice(w http.ResponseWriter, id, msg string) {
-
html := fmt.Sprintf(`<span id="%s" hx-swap-oob="innerHTML">%s</span>`, id, msg)
-
-
w.Header().Set("Content-Type", "text/html")
-
w.WriteHeader(http.StatusOK)
-
w.Write([]byte(html))
-
}
-62
routes/internal.go
···
-
package routes
-
-
import (
-
"encoding/json"
-
"net/http"
-
-
"github.com/go-chi/chi/v5"
-
"github.com/icyphox/bild/config"
-
"github.com/icyphox/bild/db"
-
)
-
-
type InternalHandle struct {
-
c *config.Config
-
db *db.DB
-
}
-
-
func SetupInternal(c *config.Config, db *db.DB) http.Handler {
-
ih := &InternalHandle{
-
c: c,
-
db: db,
-
}
-
-
r := chi.NewRouter()
-
r.Route("/internal/allkeys", func(r chi.Router) {
-
r.Get("/", ih.AllKeys)
-
})
-
-
return r
-
}
-
-
func (h *InternalHandle) returnJSON(w http.ResponseWriter, data interface{}) error {
-
w.Header().Set("Content-Type", "application/json")
-
res, err := json.Marshal(data)
-
if err != nil {
-
return err
-
}
-
_, err = w.Write(res)
-
return err
-
}
-
-
func (h *InternalHandle) returnErr(w http.ResponseWriter, err error) error {
-
w.WriteHeader(http.StatusInternalServerError)
-
return h.returnJSON(w, map[string]string{
-
"error": err.Error(),
-
})
-
}
-
-
func (h *InternalHandle) AllKeys(w http.ResponseWriter, r *http.Request) {
-
keys, err := h.db.GetAllPublicKeys()
-
if err != nil {
-
h.returnErr(w, err)
-
return
-
}
-
keyMap := map[string]string{}
-
for _, key := range keys {
-
keyMap[key.DID] = key.Key
-
}
-
if err := h.returnJSON(w, keyMap); err != nil {
-
h.returnErr(w, err)
-
return
-
}
-
}
-61
routes/middleware/did.go
···
-
package middleware
-
-
import (
-
"context"
-
"log"
-
"net/http"
-
"sync"
-
"time"
-
-
"github.com/bluesky-social/indigo/atproto/identity"
-
"github.com/icyphox/bild/auth"
-
)
-
-
type cachedIdent struct {
-
ident *identity.Identity
-
expiry time.Time
-
}
-
-
var (
-
identCache = make(map[string]cachedIdent)
-
cacheMutex sync.RWMutex
-
)
-
-
// Only use this middleware for routes that require a handle
-
// /@{user}/...
-
func AddDID(next http.Handler) http.Handler {
-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-
user := r.PathValue("user")
-
-
// Check cache first
-
cacheMutex.RLock()
-
if cached, ok := identCache[user]; ok && time.Now().Before(cached.expiry) {
-
cacheMutex.RUnlock()
-
ctx := context.WithValue(r.Context(), "did", cached.ident.DID.String())
-
r = r.WithContext(ctx)
-
next.ServeHTTP(w, r)
-
return
-
}
-
cacheMutex.RUnlock()
-
-
// Cache miss - resolve and cache
-
ident, err := auth.ResolveIdent(r.Context(), user)
-
if err != nil {
-
log.Println("error resolving identity", err)
-
http.Error(w, "error resolving identity", http.StatusNotFound)
-
return
-
}
-
-
cacheMutex.Lock()
-
identCache[user] = cachedIdent{
-
ident: ident,
-
expiry: time.Now().Add(24 * time.Hour),
-
}
-
cacheMutex.Unlock()
-
-
ctx := context.WithValue(r.Context(), "did", ident.DID.String())
-
r = r.WithContext(ctx)
-
-
next.ServeHTTP(w, r)
-
})
-
}
-643
routes/routes.go
···
-
package routes
-
-
import (
-
"compress/gzip"
-
"errors"
-
"fmt"
-
"html/template"
-
"log"
-
"net/http"
-
"os"
-
"path/filepath"
-
"sort"
-
"strconv"
-
"strings"
-
"time"
-
-
comatproto "github.com/bluesky-social/indigo/api/atproto"
-
lexutil "github.com/bluesky-social/indigo/lex/util"
-
"github.com/dustin/go-humanize"
-
"github.com/go-chi/chi/v5"
-
"github.com/go-git/go-git/v5/plumbing"
-
"github.com/google/uuid"
-
"github.com/gorilla/sessions"
-
shbild "github.com/icyphox/bild/api/bild"
-
"github.com/icyphox/bild/auth"
-
"github.com/icyphox/bild/config"
-
"github.com/icyphox/bild/db"
-
"github.com/icyphox/bild/git"
-
"github.com/russross/blackfriday/v2"
-
"golang.org/x/crypto/ssh"
-
)
-
-
type Handle struct {
-
c *config.Config
-
t *template.Template
-
s *sessions.CookieStore
-
db *db.DB
-
auth *auth.Auth
-
}
-
-
func (h *Handle) Index(w http.ResponseWriter, r *http.Request) {
-
name := displayRepoName(r)
-
path := filepath.Join(h.c.Repo.ScanPath, name)
-
dirs, err := os.ReadDir(path)
-
if err != nil {
-
h.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() || h.isIgnored(name) || h.isUnlisted(name) {
-
continue
-
}
-
-
gr, err := git.Open(path, "")
-
if err != nil {
-
log.Println(err)
-
continue
-
}
-
-
c, err := gr.LastCommit()
-
if err != nil {
-
h.Write500(w)
-
log.Println(err)
-
return
-
}
-
-
infos = append(infos, info{
-
DisplayName: trimDotGit(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)
-
})
-
-
data := make(map[string]interface{})
-
data["meta"] = h.c.Meta
-
data["info"] = infos
-
-
if err := h.t.ExecuteTemplate(w, "index", data); err != nil {
-
log.Println(err)
-
return
-
}
-
}
-
-
func (h *Handle) RepoIndex(w http.ResponseWriter, r *http.Request) {
-
name := displayRepoName(r)
-
if h.isIgnored(name) {
-
h.Write404(w)
-
return
-
}
-
-
path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
-
-
gr, err := git.Open(path, "")
-
if err != nil {
-
if errors.Is(err, plumbing.ErrReferenceNotFound) {
-
h.t.ExecuteTemplate(w, "repo/empty", nil)
-
return
-
} else {
-
h.Write404(w)
-
return
-
}
-
}
-
commits, err := gr.Commits()
-
if err != nil {
-
h.Write500(w)
-
log.Println(err)
-
return
-
}
-
-
var readmeContent template.HTML
-
for _, readme := range h.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 := sanitize(unsafe)
-
readmeContent = template.HTML(html)
-
default:
-
safe := sanitize([]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(h.c.Repo.MainBranch)
-
if err != nil {
-
h.Write500(w)
-
log.Println(err)
-
return
-
}
-
-
if len(commits) >= 3 {
-
commits = commits[:3]
-
}
-
-
data := make(map[string]any)
-
data["name"] = name
-
data["displayname"] = trimDotGit(name)
-
data["ref"] = mainBranch
-
data["readme"] = readmeContent
-
data["commits"] = commits
-
data["desc"] = getDescription(path)
-
data["servername"] = h.c.Server.Name
-
data["meta"] = h.c.Meta
-
data["gomod"] = isGoModule(gr)
-
-
if err := h.t.ExecuteTemplate(w, "repo/repo", data); err != nil {
-
log.Println(err)
-
return
-
}
-
-
return
-
}
-
-
func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) {
-
name := displayRepoName(r)
-
if h.isIgnored(name) {
-
h.Write404(w)
-
return
-
}
-
treePath := chi.URLParam(r, "*")
-
ref := chi.URLParam(r, "ref")
-
-
path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
-
gr, err := git.Open(path, ref)
-
if err != nil {
-
h.Write404(w)
-
return
-
}
-
-
files, err := gr.FileTree(treePath)
-
if err != nil {
-
h.Write500(w)
-
log.Println(err)
-
return
-
}
-
-
data := make(map[string]any)
-
data["name"] = name
-
data["displayname"] = trimDotGit(name)
-
data["ref"] = ref
-
data["parent"] = treePath
-
data["desc"] = getDescription(path)
-
data["dotdot"] = filepath.Dir(treePath)
-
-
h.listFiles(files, data, w)
-
return
-
}
-
-
func (h *Handle) 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 := displayRepoName(r)
-
-
if h.isIgnored(name) {
-
h.Write404(w)
-
return
-
}
-
treePath := chi.URLParam(r, "*")
-
ref := chi.URLParam(r, "ref")
-
-
path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
-
gr, err := git.Open(path, ref)
-
if err != nil {
-
h.Write404(w)
-
return
-
}
-
-
contents, err := gr.FileContent(treePath)
-
if err != nil {
-
h.Write500(w)
-
return
-
}
-
data := make(map[string]any)
-
data["name"] = name
-
data["displayname"] = trimDotGit(name)
-
data["ref"] = ref
-
data["desc"] = getDescription(path)
-
data["path"] = treePath
-
-
safe := sanitize([]byte(contents))
-
-
if raw {
-
h.showRaw(string(safe), w)
-
} else {
-
if h.c.Meta.SyntaxHighlight == "" {
-
h.showFile(string(safe), data, w)
-
} else {
-
h.showFileWithHighlight(treePath, string(safe), data, w)
-
}
-
}
-
}
-
-
func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) {
-
name := displayRepoName(r)
-
if h.isIgnored(name) {
-
h.Write404(w)
-
return
-
}
-
-
file := chi.URLParam(r, "file")
-
-
// TODO: extend this to add more files compression (e.g.: xz)
-
if !strings.HasSuffix(file, ".tar.gz") {
-
h.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 := filepath.Join(h.c.Repo.ScanPath, didPath(r))
-
gr, err := git.Open(path, ref)
-
if err != nil {
-
h.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 (h *Handle) Log(w http.ResponseWriter, r *http.Request) {
-
name := displayRepoName(r)
-
if h.isIgnored(name) {
-
h.Write404(w)
-
return
-
}
-
ref := chi.URLParam(r, "ref")
-
-
path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
-
gr, err := git.Open(path, ref)
-
if err != nil {
-
h.Write404(w)
-
return
-
}
-
-
commits, err := gr.Commits()
-
if err != nil {
-
h.Write500(w)
-
log.Println(err)
-
return
-
}
-
-
data := make(map[string]interface{})
-
data["commits"] = commits
-
data["meta"] = h.c.Meta
-
data["name"] = name
-
data["displayname"] = trimDotGit(name)
-
data["ref"] = ref
-
data["desc"] = getDescription(path)
-
data["log"] = true
-
-
if err := h.t.ExecuteTemplate(w, "repo/log", data); err != nil {
-
log.Println(err)
-
return
-
}
-
}
-
-
func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) {
-
name := displayRepoName(r)
-
if h.isIgnored(name) {
-
h.Write404(w)
-
return
-
}
-
ref := chi.URLParam(r, "ref")
-
-
path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
-
gr, err := git.Open(path, ref)
-
if err != nil {
-
h.Write404(w)
-
return
-
}
-
-
diff, err := gr.Diff()
-
if err != nil {
-
h.Write500(w)
-
log.Println(err)
-
return
-
}
-
-
data := make(map[string]interface{})
-
-
data["commit"] = diff.Commit
-
data["stat"] = diff.Stat
-
data["diff"] = diff.Diff
-
data["meta"] = h.c.Meta
-
data["name"] = name
-
data["displayname"] = trimDotGit(name)
-
data["ref"] = ref
-
data["desc"] = getDescription(path)
-
-
if err := h.t.ExecuteTemplate(w, "repo/commit", data); err != nil {
-
log.Println(err)
-
return
-
}
-
}
-
-
func (h *Handle) Refs(w http.ResponseWriter, r *http.Request) {
-
name := chi.URLParam(r, "name")
-
if h.isIgnored(name) {
-
h.Write404(w)
-
return
-
}
-
-
path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
-
gr, err := git.Open(path, "")
-
if err != nil {
-
h.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)
-
h.Write500(w)
-
return
-
}
-
-
data := make(map[string]interface{})
-
-
data["meta"] = h.c.Meta
-
data["name"] = name
-
data["displayname"] = trimDotGit(name)
-
data["branches"] = branches
-
data["tags"] = tags
-
data["desc"] = getDescription(path)
-
-
if err := h.t.ExecuteTemplate(w, "repo/refs", data); err != nil {
-
log.Println(err)
-
return
-
}
-
}
-
-
func (h *Handle) Collaborators(w http.ResponseWriter, r *http.Request) {
-
// put repo resolution in middleware
-
repoOwnerHandle := chi.URLParam(r, "user")
-
repoOwner, err := auth.ResolveIdent(r.Context(), repoOwnerHandle)
-
if err != nil {
-
log.Println("invalid did")
-
http.Error(w, "invalid did", http.StatusNotFound)
-
return
-
}
-
repoName := chi.URLParam(r, "name")
-
-
switch r.Method {
-
case http.MethodGet:
-
// TODO fetch a list of collaborators and their access rights
-
http.Error(w, "unimpl 1", http.StatusInternalServerError)
-
return
-
case http.MethodPut:
-
newUser := r.FormValue("newUser")
-
if newUser == "" {
-
// TODO: htmx this
-
http.Error(w, "unimpl 2", http.StatusInternalServerError)
-
return
-
}
-
newUserIdentity, err := auth.ResolveIdent(r.Context(), newUser)
-
if err != nil {
-
// TODO: htmx this
-
log.Println("invalid handle")
-
http.Error(w, "unimpl 3", http.StatusBadRequest)
-
return
-
}
-
err = h.db.SetWriter(newUserIdentity.DID.String(), repoOwner.DID.String(), repoName)
-
if err != nil {
-
// TODO: htmx this
-
log.Println("failed to add collaborator")
-
http.Error(w, "unimpl 4", http.StatusInternalServerError)
-
return
-
}
-
-
log.Println("success")
-
return
-
-
}
-
}
-
-
func (h *Handle) ServeStatic(w http.ResponseWriter, r *http.Request) {
-
f := chi.URLParam(r, "file")
-
f = filepath.Clean(filepath.Join(h.c.Dirs.Static, f))
-
-
http.ServeFile(w, r, f)
-
}
-
-
func (h *Handle) Login(w http.ResponseWriter, r *http.Request) {
-
switch r.Method {
-
case http.MethodGet:
-
if err := h.t.ExecuteTemplate(w, "user/login", nil); err != nil {
-
log.Println(err)
-
return
-
}
-
case http.MethodPost:
-
username := r.FormValue("username")
-
appPassword := r.FormValue("app_password")
-
-
atSession, err := h.auth.CreateInitialSession(w, r, username, appPassword)
-
if err != nil {
-
h.WriteOOBNotice(w, "login", "Invalid username or app password.")
-
log.Printf("creating initial session: %s", err)
-
return
-
}
-
-
err = h.auth.StoreSession(r, w, &atSession, nil)
-
if err != nil {
-
h.WriteOOBNotice(w, "login", "Failed to store session.")
-
log.Printf("storing session: %s", err)
-
return
-
}
-
-
log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did)
-
http.Redirect(w, r, "/", http.StatusSeeOther)
-
return
-
}
-
}
-
-
func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) {
-
session, _ := h.s.Get(r, "bild-session")
-
did := session.Values["did"].(string)
-
-
switch r.Method {
-
case http.MethodGet:
-
keys, err := h.db.GetPublicKeys(did)
-
if err != nil {
-
h.WriteOOBNotice(w, "keys", "Failed to list keys. Try again later.")
-
log.Println(err)
-
return
-
}
-
-
data := make(map[string]interface{})
-
data["keys"] = keys
-
if err := h.t.ExecuteTemplate(w, "settings/keys", data); err != nil {
-
log.Println(err)
-
return
-
}
-
case http.MethodPut:
-
key := r.FormValue("key")
-
name := r.FormValue("name")
-
client, _ := h.auth.AuthorizedClient(r)
-
-
_, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))
-
if err != nil {
-
h.WriteOOBNotice(w, "keys", "Invalid public key. Check your formatting and try again.")
-
log.Printf("parsing public key: %s", err)
-
return
-
}
-
-
if err := h.db.AddPublicKey(did, name, key); err != nil {
-
h.WriteOOBNotice(w, "keys", "Failed to add key.")
-
log.Printf("adding public key: %s", err)
-
return
-
}
-
-
// store in pds too
-
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
-
Collection: "sh.bild.publicKey",
-
Repo: did,
-
Rkey: uuid.New().String(),
-
Record: &lexutil.LexiconTypeDecoder{Val: &shbild.PublicKey{
-
Created: time.Now().String(),
-
Key: key,
-
Name: name,
-
}},
-
})
-
-
// invalid record
-
if err != nil {
-
h.WriteOOBNotice(w, "keys", "Invalid inputs. Check your formatting and try again.")
-
log.Printf("failed to create record: %s", err)
-
return
-
}
-
-
log.Println("created atproto record: ", resp.Uri)
-
-
h.WriteOOBNotice(w, "keys", "Key added!")
-
return
-
}
-
}
-
-
func (h *Handle) NewRepo(w http.ResponseWriter, r *http.Request) {
-
session, _ := h.s.Get(r, "bild-session")
-
did := session.Values["did"].(string)
-
handle := session.Values["handle"].(string)
-
-
switch r.Method {
-
case http.MethodGet:
-
if err := h.t.ExecuteTemplate(w, "repo/new", nil); err != nil {
-
log.Println(err)
-
return
-
}
-
case http.MethodPut:
-
name := r.FormValue("name")
-
description := r.FormValue("description")
-
-
repoPath := filepath.Join(h.c.Repo.ScanPath, did, name)
-
err := git.InitBare(repoPath)
-
if err != nil {
-
h.WriteOOBNotice(w, "repo", "Error creating repo. Try again later.")
-
return
-
}
-
-
// For use by repoguard
-
didPath := filepath.Join(repoPath, "did")
-
err = os.WriteFile(didPath, []byte(did), 0644)
-
if err != nil {
-
h.WriteOOBNotice(w, "repo", "Error creating repo. Try again later.")
-
return
-
}
-
-
// TODO: add repo & setting-to-owner must happen in the same transaction
-
err = h.db.AddRepo(did, name, description)
-
if err != nil {
-
log.Println(err)
-
h.WriteOOBNotice(w, "repo", "Error creating repo. Try again later.")
-
return
-
}
-
// current user is set to owner of did/name repo
-
err = h.db.SetOwner(did, did, name)
-
if err != nil {
-
log.Println(err)
-
h.WriteOOBNotice(w, "repo", "Error creating repo. Try again later.")
-
return
-
}
-
-
w.Header().Set("HX-Redirect", fmt.Sprintf("/@%s/%s", handle, name))
-
w.WriteHeader(http.StatusOK)
-
}
-
}
-
-
func (h *Handle) Timeline(w http.ResponseWriter, r *http.Request) {
-
session, err := h.s.Get(r, "bild-session")
-
user := make(map[string]string)
-
if err != nil || session.IsNew {
-
// user is not logged in
-
} else {
-
user["handle"] = session.Values["handle"].(string)
-
user["did"] = session.Values["did"].(string)
-
}
-
-
if err := h.t.ExecuteTemplate(w, "timeline", user); err != nil {
-
log.Println(err)
-
return
-
}
-
}
-54
routes/tmpl/tmpl.go
···
-
package tmpl
-
-
import (
-
"html/template"
-
"log"
-
"os"
-
"path/filepath"
-
"strings"
-
)
-
-
func Load(tpath string) (*template.Template, error) {
-
tmpl := template.New("")
-
loadedTemplates := make(map[string]bool)
-
-
err := filepath.Walk(tpath, func(path string, info os.FileInfo, err error) error {
-
if err != nil {
-
return err
-
}
-
-
if !info.IsDir() && strings.HasSuffix(path, ".html") {
-
content, err := os.ReadFile(path)
-
if err != nil {
-
return err
-
}
-
-
relPath, err := filepath.Rel(tpath, path)
-
if err != nil {
-
return err
-
}
-
-
name := strings.TrimSuffix(relPath, ".html")
-
name = strings.ReplaceAll(name, string(filepath.Separator), "/")
-
-
_, err = tmpl.New(name).Parse(string(content))
-
if err != nil {
-
log.Printf("error parsing template %s: %v", name, err)
-
return err
-
}
-
-
loadedTemplates[name] = true
-
log.Printf("loaded template: %s", name)
-
return err
-
}
-
return nil
-
})
-
-
if err != nil {
-
return nil, err
-
}
-
-
log.Printf("total templates loaded: %d", len(loadedTemplates))
-
return tmpl, nil
-
-
}
-146
routes/util.go
···
-
package routes
-
-
import (
-
"fmt"
-
"io/fs"
-
"log"
-
"net/http"
-
"os"
-
"path/filepath"
-
"strings"
-
-
"github.com/go-chi/chi/v5"
-
"github.com/icyphox/bild/auth"
-
"github.com/icyphox/bild/git"
-
"github.com/microcosm-cc/bluemonday"
-
)
-
-
func sanitize(content []byte) []byte {
-
return bluemonday.UGCPolicy().SanitizeBytes([]byte(content))
-
}
-
-
func isGoModule(gr *git.GitRepo) bool {
-
_, err := gr.FileContent("go.mod")
-
return err == nil
-
}
-
-
func displayRepoName(r *http.Request) string {
-
user := r.Context().Value("did").(string)
-
name := chi.URLParam(r, "name")
-
-
handle, err := auth.ResolveIdent(r.Context(), user)
-
if err != nil {
-
log.Printf("failed to resolve ident: %s: %s", user, err)
-
return fmt.Sprintf("%s/%s", user, name)
-
}
-
-
return fmt.Sprintf("@%s/%s", handle.Handle.String(), name)
-
}
-
-
func didPath(r *http.Request) string {
-
did := r.Context().Value("did").(string)
-
path := filepath.Join(did, chi.URLParam(r, "name"))
-
filepath.Clean(path)
-
return path
-
}
-
-
func trimDotGit(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 (h *Handle) isUnlisted(name string) bool {
-
for _, i := range h.c.Repo.Unlisted {
-
if name == i {
-
return true
-
}
-
}
-
-
return false
-
}
-
-
func (h *Handle) isIgnored(name string) bool {
-
for _, i := range h.c.Repo.Ignore {
-
if name == i {
-
return true
-
}
-
}
-
-
return false
-
}
-
-
type repoInfo struct {
-
Git *git.GitRepo
-
Path string
-
Category string
-
}
-
-
func (d *Handle) 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 *Handle) 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)
-
}
static/legit.png

This is a binary file and will not be displayed.

-341
static/style.css
···
-
: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;
-
--white: #000;
-
--light: #181818;
-
--cyan: #76c7c0;
-
--light-gray: #333;
-
--medium-gray: #444;
-
--gray: #aaa;
-
--dark: #ddd;
-
--darker: #f4f4f4;
-
}
-
}
-
-
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;
-
}
-
}
-10
templates/errors/404.html
···
-
<html>
-
<title>404</title>
-
{{ template "layouts/head" . }}
-
<body>
-
{{ template "layouts/nav" . }}
-
<main>
-
<h3>404 &mdash; nothing like that here.</h3>
-
</main>
-
</body>
-
</html>
-10
templates/errors/500.html
···
-
<html>
-
<title>500</title>
-
{{ template "layouts/head" . }}
-
<body>
-
{{ template "layouts/nav" . }}
-
<main>
-
<h3>500 &mdash; something broke!</h3>
-
</main>
-
</body>
-
</html>
-21
templates/index.html
···
-
{{ define "index" }}
-
<html>
-
{{ template "layouts/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 }}
-37
templates/layouts/head.html
···
-
<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" />
-
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
-
<meta name="htmx-config" content='{"selfRequestsOnly":false}' />
-
-
{{ 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>
-12
templates/layouts/nav.html
···
-
<nav>
-
<ul>
-
{{ if .name }}
-
<li><a href="/{{ .name }}">summary</a></li>
-
<li><a href="/{{ .name }}/refs">refs</a> {{ if .ref }}</li>
-
-
<li><a href="/{{ .name }}/tree/{{ .ref }}/">tree</a></li>
-
<li>
-
<a href="/{{ .name }}/log/{{ .ref }}">log</a> {{ end }} {{ end }}
-
</li>
-
</ul>
-
</nav>
-9
templates/layouts/repo-header.html
···
-
<header>
-
<h2>
-
<a href="/">all repos</a>
-
&mdash; {{ .displayname }} {{ if .ref }}
-
<span class="ref">@ {{ .ref }}</span>
-
{{ end }}
-
</h2>
-
<h3 class="desc">{{ .desc }}</h3>
-
</header>
-12
templates/layouts/topbar.html
···
-
<nav>
-
<ul>
-
{{ if . }}
-
<li>logged in as
-
<a href="/@{{ .handle }}">{{ .handle }}</a> (with {{ .did }})
-
</li>
-
{{ else }}
-
<li><a href="/login">login</a></li>
-
{{ end }}
-
</ul>
-
</nav>
-
-100
templates/repo/commit.html
···
-
<html>
-
{{ template "layouts/head" . }}
-
-
{{ template "layouts/repo-header" . }}
-
<body>
-
{{ template "layouts/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>
-9
templates/repo/empty.html
···
-
<html>
-
{{ template "layouts/head" . }}
-
-
<body>
-
<main>
-
<p>This is an empty Git repository. Push some commits here.</p>
-
</main>
-
</body>
-
</html>
-34
templates/repo/file.html
···
-
<html>
-
{{ template "layouts/head" . }}
-
{{ template "layouts/repo-header" . }}
-
<body>
-
{{ template "layouts/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>
-23
templates/repo/log.html
···
-
<html>
-
{{ template "layouts/head" . }}
-
-
{{ template "layouts/repo-header" . }}
-
<body>
-
{{ template "layouts/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>
-30
templates/repo/new.html
···
-
<html>
-
{{ template "layouts/head" . }}
-
-
<body>
-
<main>
-
<h1>create a new repository</h1>
-
<form>
-
<p>
-
Give your Git repository a name and, optionally, a
-
description.
-
</p>
-
<div id="repo"></div>
-
<div>
-
<label for="name">Name</label>
-
<input type="text" id="name" name="name" placeholder="" />
-
<label for="description">Description</label>
-
<input
-
type="text"
-
id="description"
-
name="description"
-
placeholder=""
-
/>
-
</div>
-
<button hx-put="/repo/new" hx-swap="none" type="submit">
-
Submit
-
</button>
-
</form>
-
</main>
-
</body>
-
</html>
-38
templates/repo/refs.html
···
-
<html>
-
{{ template "layouts/head" . }}
-
-
{{ template "layouts/repo-header" . }}
-
<body>
-
{{ template "layouts/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>
-36
templates/repo/repo.html
···
-
<html>
-
{{ template "layouts/head" . }}
-
-
{{ template "layouts/repo-header" . }}
-
-
<body>
-
{{ template "layouts/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>
-53
templates/repo/tree.html
···
-
<html>
-
-
{{ template "layouts/head" . }}
-
-
{{ template "layouts/repo-header" . }}
-
<body>
-
{{ template "layouts/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>
-51
templates/settings/keys.html
···
-
<html>
-
{{ template "layouts/head" . }}
-
-
<body>
-
<main>
-
<form>
-
<p>
-
Give your key a name and paste your
-
<strong>public</strong> key here. This is what you'll use to
-
push to your Git repository.
-
</p>
-
<div id="keys"></div>
-
<div>
-
<input
-
type="text"
-
id="name"
-
name="name"
-
placeholder="my laptop"
-
/>
-
<input
-
type="text"
-
id="public_key"
-
name="key"
-
placeholder="ssh-ed25519 AAABBBHUNTER2..."
-
/>
-
</div>
-
<button hx-put="/settings/keys" hx-swap="none" type="submit">
-
Submit
-
</button>
-
</form>
-
<table>
-
<thead>
-
<tr>
-
<th>Key</th>
-
<th>Name</th>
-
<th>Created</th>
-
</tr>
-
</thead>
-
<tbody>
-
{{ range .keys }}
-
<tr>
-
<td>{{ .Name }}</td>
-
<td>{{ .Created }}</td>
-
<td>{{ .Key }}</td>
-
</tr>
-
{{ end }}
-
</tbody>
-
</table>
-
</main>
-
</body>
-
</html>
-14
templates/timeline.html
···
-
{{ define "timeline" }}
-
<html>
-
{{ template "layouts/head" . }}
-
-
<header>
-
<h1>timeline</h1>
-
</header>
-
<body>
-
<main>
-
{{ template "layouts/topbar" . }}
-
</main>
-
</body>
-
</html>
-
{{ end }}
-29
templates/user/login.html
···
-
<html>
-
{{ template "layouts/head" . }}
-
-
<body>
-
<main>
-
<form class="form-login" method="post" action="/login">
-
<p>
-
You will be redirected to bsky.social (or your PDS) to
-
complete login.
-
</p>
-
<div>
-
<input
-
type="text"
-
id="username"
-
name="username"
-
placeholder="@username.bsky.social"
-
/>
-
<input
-
type="password"
-
id="app_password"
-
name="app_password"
-
placeholder="app password"
-
/>
-
</div>
-
<button type="submit">Login</button>
-
</form>
-
</main>
-
</body>
-
</html>