-122
auth/auth.go
-122
auth/auth.go
···
1
-
package auth
2
-
3
-
import (
4
-
"context"
5
-
"fmt"
6
-
"net/http"
7
-
"time"
8
-
9
-
comatproto "github.com/bluesky-social/indigo/api/atproto"
10
-
"github.com/bluesky-social/indigo/atproto/identity"
11
-
"github.com/bluesky-social/indigo/atproto/syntax"
12
-
"github.com/bluesky-social/indigo/xrpc"
13
-
"github.com/gorilla/sessions"
14
-
)
15
-
16
-
type Auth struct {
17
-
s sessions.Store
18
-
}
19
-
20
-
func NewAuth(store sessions.Store) *Auth {
21
-
return &Auth{store}
22
-
}
23
-
24
-
func ResolveIdent(ctx context.Context, arg string) (*identity.Identity, error) {
25
-
id, err := syntax.ParseAtIdentifier(arg)
26
-
if err != nil {
27
-
return nil, err
28
-
}
29
-
30
-
dir := identity.DefaultDirectory()
31
-
return dir.Lookup(ctx, *id)
32
-
}
33
-
34
-
func (a *Auth) AuthorizedClient(r *http.Request) (*xrpc.Client, error) {
35
-
clientSession, err := a.s.Get(r, "bild-session")
36
-
37
-
if err != nil || clientSession.IsNew {
38
-
return nil, err
39
-
}
40
-
41
-
did := clientSession.Values["did"].(string)
42
-
pdsUrl := clientSession.Values["pds"].(string)
43
-
accessJwt := clientSession.Values["accessJwt"].(string)
44
-
refreshJwt := clientSession.Values["refreshJwt"].(string)
45
-
46
-
client := &xrpc.Client{
47
-
Host: pdsUrl,
48
-
Auth: &xrpc.AuthInfo{
49
-
AccessJwt: accessJwt,
50
-
RefreshJwt: refreshJwt,
51
-
Did: did,
52
-
},
53
-
}
54
-
55
-
return client, nil
56
-
}
57
-
58
-
func (a *Auth) CreateInitialSession(w http.ResponseWriter, r *http.Request, username, appPassword string) (AtSessionCreate, error) {
59
-
ctx := r.Context()
60
-
resolved, err := ResolveIdent(ctx, username)
61
-
if err != nil {
62
-
return AtSessionCreate{}, fmt.Errorf("invalid handle: %s", err)
63
-
}
64
-
65
-
pdsUrl := resolved.PDSEndpoint()
66
-
client := xrpc.Client{
67
-
Host: pdsUrl,
68
-
}
69
-
70
-
atSession, err := comatproto.ServerCreateSession(ctx, &client, &comatproto.ServerCreateSession_Input{
71
-
Identifier: resolved.DID.String(),
72
-
Password: appPassword,
73
-
})
74
-
if err != nil {
75
-
return AtSessionCreate{}, fmt.Errorf("invalid app password")
76
-
}
77
-
78
-
return AtSessionCreate{
79
-
ServerCreateSession_Output: *atSession,
80
-
PDSEndpoint: pdsUrl,
81
-
}, nil
82
-
}
83
-
84
-
func (a *Auth) StoreSession(r *http.Request, w http.ResponseWriter, atSessionCreate *AtSessionCreate, atSessionRefresh *AtSessionRefresh) error {
85
-
if atSessionCreate != nil {
86
-
atSession := atSessionCreate
87
-
88
-
clientSession, _ := a.s.Get(r, "bild-session")
89
-
clientSession.Values["handle"] = atSession.Handle
90
-
clientSession.Values["did"] = atSession.Did
91
-
clientSession.Values["accessJwt"] = atSession.AccessJwt
92
-
clientSession.Values["refreshJwt"] = atSession.RefreshJwt
93
-
clientSession.Values["expiry"] = time.Now().Add(time.Hour).String()
94
-
clientSession.Values["pds"] = atSession.PDSEndpoint
95
-
clientSession.Values["authenticated"] = true
96
-
97
-
return clientSession.Save(r, w)
98
-
} else {
99
-
atSession := atSessionRefresh
100
-
101
-
clientSession, _ := a.s.Get(r, "bild-session")
102
-
clientSession.Values["handle"] = atSession.Handle
103
-
clientSession.Values["did"] = atSession.Did
104
-
clientSession.Values["accessJwt"] = atSession.AccessJwt
105
-
clientSession.Values["refreshJwt"] = atSession.RefreshJwt
106
-
clientSession.Values["expiry"] = time.Now().Add(time.Hour).String()
107
-
clientSession.Values["pds"] = atSession.PDSEndpoint
108
-
clientSession.Values["authenticated"] = true
109
-
110
-
return clientSession.Save(r, w)
111
-
}
112
-
}
113
-
114
-
func (a *Auth) GetSessionUser(r *http.Request) (*identity.Identity, error) {
115
-
session, _ := a.s.Get(r, "bild-session")
116
-
did, ok := session.Values["did"].(string)
117
-
if !ok {
118
-
return nil, fmt.Errorf("user is not authenticated")
119
-
}
120
-
121
-
return ResolveIdent(r.Context(), did)
122
-
}
-15
auth/types.go
-15
auth/types.go
···
1
-
package auth
2
-
3
-
import (
4
-
comatproto "github.com/bluesky-social/indigo/api/atproto"
5
-
)
6
-
7
-
type AtSessionCreate struct {
8
-
comatproto.ServerCreateSession_Output
9
-
PDSEndpoint string
10
-
}
11
-
12
-
type AtSessionRefresh struct {
13
-
comatproto.ServerRefreshSession_Output
14
-
PDSEndpoint string
15
-
}
-47
cmd/legit/main.go
-47
cmd/legit/main.go
···
1
-
package main
2
-
3
-
import (
4
-
"flag"
5
-
"fmt"
6
-
"log"
7
-
"log/slog"
8
-
"net/http"
9
-
"os"
10
-
11
-
"github.com/icyphox/bild/config"
12
-
"github.com/icyphox/bild/db"
13
-
"github.com/icyphox/bild/routes"
14
-
)
15
-
16
-
func main() {
17
-
var cfg string
18
-
flag.StringVar(&cfg, "config", "./config.yaml", "path to config file")
19
-
flag.Parse()
20
-
21
-
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, nil)))
22
-
23
-
c, err := config.Read(cfg)
24
-
if err != nil {
25
-
log.Fatal(err)
26
-
}
27
-
db, err := db.Setup(c.Server.DBPath)
28
-
if err != nil {
29
-
log.Fatalf("failed to setup db: %s", err)
30
-
}
31
-
32
-
mux, err := routes.Setup(c, db)
33
-
if err != nil {
34
-
log.Fatal(err)
35
-
}
36
-
37
-
internalMux := routes.SetupInternal(c, db)
38
-
39
-
addr := fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port)
40
-
internalAddr := fmt.Sprintf("%s:%d", c.Server.InternalHost, c.Server.InternalPort)
41
-
42
-
log.Println("starting main server on", addr)
43
-
go http.ListenAndServe(addr, mux)
44
-
45
-
log.Println("starting internal server on", internalAddr)
46
-
log.Fatal(http.ListenAndServe(internalAddr, internalMux))
47
-
}
-61
config/config.go
-61
config/config.go
···
1
-
package config
2
-
3
-
import (
4
-
"fmt"
5
-
"os"
6
-
"path/filepath"
7
-
8
-
"gopkg.in/yaml.v3"
9
-
)
10
-
11
-
type Config struct {
12
-
Repo struct {
13
-
ScanPath string `yaml:"scanPath"`
14
-
Readme []string `yaml:"readme"`
15
-
MainBranch []string `yaml:"mainBranch"`
16
-
Ignore []string `yaml:"ignore,omitempty"`
17
-
Unlisted []string `yaml:"unlisted,omitempty"`
18
-
} `yaml:"repo"`
19
-
Dirs struct {
20
-
Templates string `yaml:"templates"`
21
-
Static string `yaml:"static"`
22
-
} `yaml:"dirs"`
23
-
Meta struct {
24
-
Title string `yaml:"title"`
25
-
Description string `yaml:"description"`
26
-
SyntaxHighlight string `yaml:"syntaxHighlight"`
27
-
} `yaml:"meta"`
28
-
Server struct {
29
-
Name string `yaml:"name,omitempty"`
30
-
Host string `yaml:"host"`
31
-
Port int `yaml:"port"`
32
-
DBPath string `yaml:"dbpath"`
33
-
34
-
InternalHost string `yaml:"internalHost,omitempty"`
35
-
InternalPort int `yaml:"internalPort,omitempty"`
36
-
} `yaml:"server"`
37
-
}
38
-
39
-
func Read(f string) (*Config, error) {
40
-
b, err := os.ReadFile(f)
41
-
if err != nil {
42
-
return nil, fmt.Errorf("reading config: %w", err)
43
-
}
44
-
45
-
c := Config{}
46
-
if err := yaml.Unmarshal(b, &c); err != nil {
47
-
return nil, fmt.Errorf("parsing config: %w", err)
48
-
}
49
-
50
-
if c.Repo.ScanPath, err = filepath.Abs(c.Repo.ScanPath); err != nil {
51
-
return nil, err
52
-
}
53
-
if c.Dirs.Templates, err = filepath.Abs(c.Dirs.Templates); err != nil {
54
-
return nil, err
55
-
}
56
-
if c.Dirs.Static, err = filepath.Abs(c.Dirs.Static); err != nil {
57
-
return nil, err
58
-
}
59
-
60
-
return &c, nil
61
-
}
-22
contrib/Dockerfile
-22
contrib/Dockerfile
···
1
-
FROM golang:1.22-alpine AS builder
2
-
3
-
WORKDIR /app
4
-
5
-
COPY . .
6
-
RUN go mod download
7
-
RUN go mod verify
8
-
9
-
RUN go build -o legit
10
-
11
-
FROM scratch AS build-release-stage
12
-
13
-
WORKDIR /app
14
-
15
-
COPY static ./static
16
-
COPY templates ./templates
17
-
COPY config.yaml ./
18
-
COPY --from=builder /app/legit ./
19
-
20
-
EXPOSE 5555
21
-
22
-
CMD ["./legit"]
-14
contrib/docker-compose.yml
-14
contrib/docker-compose.yml
···
1
-
services:
2
-
legit:
3
-
container_name: legit
4
-
build:
5
-
context: ../
6
-
dockerfile: contrib/Dockerfile
7
-
restart: unless-stopped
8
-
ports:
9
-
- "5555:5555"
10
-
volumes:
11
-
- /var/www/git:/var/www/git
12
-
- ../config.yaml:/app/config.yaml
13
-
- ../static:/app/static
14
-
- ../templates:/app/templates
-17
contrib/legit.service
-17
contrib/legit.service
···
1
-
[Unit]
2
-
Description=legit Server
3
-
After=network-online.target
4
-
Requires=network-online.target
5
-
6
-
[Service]
7
-
User=git
8
-
Group=git
9
-
ExecStart=/usr/bin/legit -config /etc/legit/config.yaml
10
-
ProtectSystem=strict
11
-
ProtectHome=strict
12
-
NoNewPrivileges=true
13
-
PrivateTmp=true
14
-
PrivateDevices=true
15
-
16
-
[Install]
17
-
WantedBy=multi-user.target
-80
db/access.go
-80
db/access.go
···
1
-
package db
2
-
3
-
import (
4
-
"log"
5
-
"strings"
6
-
)
7
-
8
-
// forms a poset
9
-
type Level int
10
-
11
-
const (
12
-
Reader Level = iota
13
-
Writer
14
-
Owner
15
-
)
16
-
17
-
var (
18
-
levelMap = map[string]Level{
19
-
"writer": Writer,
20
-
"owner": Owner,
21
-
}
22
-
)
23
-
24
-
func ParseLevel(str string) (Level, bool) {
25
-
c, ok := levelMap[strings.ToLower(str)]
26
-
return c, ok
27
-
}
28
-
29
-
func (l Level) String() string {
30
-
switch l {
31
-
case Owner:
32
-
return "OWNER"
33
-
case Writer:
34
-
return "WRITER"
35
-
case Reader:
36
-
return "READER"
37
-
default:
38
-
return "READER"
39
-
}
40
-
}
41
-
42
-
func (d *DB) SetAccessLevel(userDid string, repoDid string, repoName string, level Level) error {
43
-
_, err := d.db.Exec(
44
-
`insert
45
-
into access_levels (repo_id, did, access)
46
-
values ((select id from repos where did = $1 and name = $2), $3, $4)
47
-
on conflict (repo_id, did)
48
-
do update set access = $4;`,
49
-
repoDid, repoName, userDid, level.String())
50
-
return err
51
-
}
52
-
53
-
func (d *DB) SetOwner(userDid string, repoDid string, repoName string) error {
54
-
return d.SetAccessLevel(userDid, repoDid, repoName, Owner)
55
-
}
56
-
57
-
func (d *DB) SetWriter(userDid string, repoDid string, repoName string) error {
58
-
return d.SetAccessLevel(userDid, repoDid, repoName, Writer)
59
-
}
60
-
61
-
func (d *DB) GetAccessLevel(userDid string, repoDid string, repoName string) (Level, error) {
62
-
row := d.db.QueryRow(`
63
-
select access_levels.access
64
-
from repos
65
-
join access_levels
66
-
on repos.id = access_levels.repo_id
67
-
where access_levels.did = ? and repos.did = ? and repos.name = ?
68
-
`, userDid, repoDid, repoName)
69
-
70
-
var levelStr string
71
-
err := row.Scan(&levelStr)
72
-
if err != nil {
73
-
log.Println(err)
74
-
return Reader, err
75
-
} else {
76
-
level, _ := ParseLevel(levelStr)
77
-
return level, nil
78
-
}
79
-
80
-
}
-51
db/init.go
-51
db/init.go
···
1
-
package db
2
-
3
-
import (
4
-
"database/sql"
5
-
6
-
_ "github.com/mattn/go-sqlite3"
7
-
)
8
-
9
-
type DB struct {
10
-
db *sql.DB
11
-
}
12
-
13
-
func Setup(dbPath string) (*DB, error) {
14
-
db, err := sql.Open("sqlite3", dbPath)
15
-
if err != nil {
16
-
return nil, err
17
-
}
18
-
19
-
_, err = db.Exec(`
20
-
create table if not exists public_keys (
21
-
id integer primary key autoincrement,
22
-
did text not null,
23
-
name text not null,
24
-
key text not null,
25
-
created timestamp default current_timestamp,
26
-
unique(did, name, key)
27
-
);
28
-
create table if not exists repos (
29
-
id integer primary key autoincrement,
30
-
did text not null,
31
-
name text not null,
32
-
description text not null,
33
-
created timestamp default current_timestamp,
34
-
unique(did, name)
35
-
);
36
-
create table if not exists access_levels (
37
-
id integer primary key autoincrement,
38
-
repo_id integer not null,
39
-
did text not null,
40
-
access text not null check (access in ('OWNER', 'WRITER')),
41
-
created timestamp default current_timestamp,
42
-
unique(repo_id, did),
43
-
foreign key (repo_id) references repos(id) on delete cascade
44
-
);
45
-
`)
46
-
if err != nil {
47
-
return nil, err
48
-
}
49
-
50
-
return &DB{db: db}, nil
51
-
}
-70
db/pubkeys.go
-70
db/pubkeys.go
···
1
-
package db
2
-
3
-
import "time"
4
-
5
-
func (d *DB) AddPublicKey(did, name, key string) error {
6
-
query := `insert into public_keys (did, name, key) values (?, ?, ?)`
7
-
_, err := d.db.Exec(query, did, name, key)
8
-
return err
9
-
}
10
-
11
-
func (d *DB) RemovePublicKey(did string) error {
12
-
query := `delete from public_keys where did = ?`
13
-
_, err := d.db.Exec(query, did)
14
-
return err
15
-
}
16
-
17
-
type PublicKey struct {
18
-
Key string
19
-
Name string
20
-
DID string
21
-
Created time.Time
22
-
}
23
-
24
-
func (d *DB) GetAllPublicKeys() ([]PublicKey, error) {
25
-
var keys []PublicKey
26
-
27
-
rows, err := d.db.Query(`select key, name, did, created from public_keys`)
28
-
if err != nil {
29
-
return nil, err
30
-
}
31
-
defer rows.Close()
32
-
33
-
for rows.Next() {
34
-
var publicKey PublicKey
35
-
if err := rows.Scan(&publicKey.Key, &publicKey.Name, &publicKey.DID, &publicKey.Created); err != nil {
36
-
return nil, err
37
-
}
38
-
keys = append(keys, publicKey)
39
-
}
40
-
41
-
if err := rows.Err(); err != nil {
42
-
return nil, err
43
-
}
44
-
45
-
return keys, nil
46
-
}
47
-
48
-
func (d *DB) GetPublicKeys(did string) ([]PublicKey, error) {
49
-
var keys []PublicKey
50
-
51
-
rows, err := d.db.Query(`select did, key, name, created from public_keys where did = ?`, did)
52
-
if err != nil {
53
-
return nil, err
54
-
}
55
-
defer rows.Close()
56
-
57
-
for rows.Next() {
58
-
var publicKey PublicKey
59
-
if err := rows.Scan(&publicKey.DID, &publicKey.Key, &publicKey.Name, &publicKey.Created); err != nil {
60
-
return nil, err
61
-
}
62
-
keys = append(keys, publicKey)
63
-
}
64
-
65
-
if err := rows.Err(); err != nil {
66
-
return nil, err
67
-
}
68
-
69
-
return keys, nil
70
-
}
-25
db/repo.go
-25
db/repo.go
···
1
-
package db
2
-
3
-
func (d *DB) AddRepo(did string, name string, description string) error {
4
-
_, err := d.db.Exec("insert into repos (did, name, description) values (?, ?, ?)", did, name, description)
5
-
if err != nil {
6
-
return err
7
-
}
8
-
return nil
9
-
}
10
-
11
-
func (d *DB) RemoveRepo(did string) error {
12
-
_, err := d.db.Exec("delete from repos where did = ?", did)
13
-
if err != nil {
14
-
return err
15
-
}
16
-
return nil
17
-
}
18
-
19
-
func (d *DB) UpdateRepo(did string, name string, description string) error {
20
-
_, err := d.db.Exec("update repos set name = ?, description = ? where did = ?", name, description, did)
21
-
if err != nil {
22
-
return err
23
-
}
24
-
return nil
25
-
}
-119
git/diff.go
-119
git/diff.go
···
1
-
package git
2
-
3
-
import (
4
-
"fmt"
5
-
"log"
6
-
"strings"
7
-
8
-
"github.com/bluekeyes/go-gitdiff/gitdiff"
9
-
"github.com/go-git/go-git/v5/plumbing/object"
10
-
)
11
-
12
-
type TextFragment struct {
13
-
Header string
14
-
Lines []gitdiff.Line
15
-
}
16
-
17
-
type Diff struct {
18
-
Name struct {
19
-
Old string
20
-
New string
21
-
}
22
-
TextFragments []TextFragment
23
-
IsBinary bool
24
-
IsNew bool
25
-
IsDelete bool
26
-
}
27
-
28
-
// A nicer git diff representation.
29
-
type NiceDiff struct {
30
-
Commit struct {
31
-
Message string
32
-
Author object.Signature
33
-
This string
34
-
Parent string
35
-
}
36
-
Stat struct {
37
-
FilesChanged int
38
-
Insertions int
39
-
Deletions int
40
-
}
41
-
Diff []Diff
42
-
}
43
-
44
-
func (g *GitRepo) Diff() (*NiceDiff, error) {
45
-
c, err := g.r.CommitObject(g.h)
46
-
if err != nil {
47
-
return nil, fmt.Errorf("commit object: %w", err)
48
-
}
49
-
50
-
patch := &object.Patch{}
51
-
commitTree, err := c.Tree()
52
-
parent := &object.Commit{}
53
-
if err == nil {
54
-
parentTree := &object.Tree{}
55
-
if c.NumParents() != 0 {
56
-
parent, err = c.Parents().Next()
57
-
if err == nil {
58
-
parentTree, err = parent.Tree()
59
-
if err == nil {
60
-
patch, err = parentTree.Patch(commitTree)
61
-
if err != nil {
62
-
return nil, fmt.Errorf("patch: %w", err)
63
-
}
64
-
}
65
-
}
66
-
} else {
67
-
patch, err = parentTree.Patch(commitTree)
68
-
if err != nil {
69
-
return nil, fmt.Errorf("patch: %w", err)
70
-
}
71
-
}
72
-
}
73
-
74
-
diffs, _, err := gitdiff.Parse(strings.NewReader(patch.String()))
75
-
if err != nil {
76
-
log.Println(err)
77
-
}
78
-
79
-
nd := NiceDiff{}
80
-
nd.Commit.This = c.Hash.String()
81
-
82
-
if parent.Hash.IsZero() {
83
-
nd.Commit.Parent = ""
84
-
} else {
85
-
nd.Commit.Parent = parent.Hash.String()
86
-
}
87
-
nd.Commit.Author = c.Author
88
-
nd.Commit.Message = c.Message
89
-
90
-
for _, d := range diffs {
91
-
ndiff := Diff{}
92
-
ndiff.Name.New = d.NewName
93
-
ndiff.Name.Old = d.OldName
94
-
ndiff.IsBinary = d.IsBinary
95
-
ndiff.IsNew = d.IsNew
96
-
ndiff.IsDelete = d.IsDelete
97
-
98
-
for _, tf := range d.TextFragments {
99
-
ndiff.TextFragments = append(ndiff.TextFragments, TextFragment{
100
-
Header: tf.Header(),
101
-
Lines: tf.Lines,
102
-
})
103
-
for _, l := range tf.Lines {
104
-
switch l.Op {
105
-
case gitdiff.OpAdd:
106
-
nd.Stat.Insertions += 1
107
-
case gitdiff.OpDelete:
108
-
nd.Stat.Deletions += 1
109
-
}
110
-
}
111
-
}
112
-
113
-
nd.Diff = append(nd.Diff, ndiff)
114
-
}
115
-
116
-
nd.Stat.FilesChanged = len(diffs)
117
-
118
-
return &nd, nil
119
-
}
-345
git/git.go
-345
git/git.go
···
1
-
package git
2
-
3
-
import (
4
-
"archive/tar"
5
-
"fmt"
6
-
"io"
7
-
"io/fs"
8
-
"path"
9
-
"sort"
10
-
"time"
11
-
12
-
"github.com/go-git/go-git/v5"
13
-
"github.com/go-git/go-git/v5/plumbing"
14
-
"github.com/go-git/go-git/v5/plumbing/object"
15
-
)
16
-
17
-
type GitRepo struct {
18
-
r *git.Repository
19
-
h plumbing.Hash
20
-
}
21
-
22
-
type TagList struct {
23
-
refs []*TagReference
24
-
r *git.Repository
25
-
}
26
-
27
-
// TagReference is used to list both tag and non-annotated tags.
28
-
// Non-annotated tags should only contains a reference.
29
-
// Annotated tags should contain its reference and its tag information.
30
-
type TagReference struct {
31
-
ref *plumbing.Reference
32
-
tag *object.Tag
33
-
}
34
-
35
-
// infoWrapper wraps the property of a TreeEntry so it can export fs.FileInfo
36
-
// to tar WriteHeader
37
-
type infoWrapper struct {
38
-
name string
39
-
size int64
40
-
mode fs.FileMode
41
-
modTime time.Time
42
-
isDir bool
43
-
}
44
-
45
-
func (self *TagList) Len() int {
46
-
return len(self.refs)
47
-
}
48
-
49
-
func (self *TagList) Swap(i, j int) {
50
-
self.refs[i], self.refs[j] = self.refs[j], self.refs[i]
51
-
}
52
-
53
-
// sorting tags in reverse chronological order
54
-
func (self *TagList) Less(i, j int) bool {
55
-
var dateI time.Time
56
-
var dateJ time.Time
57
-
58
-
if self.refs[i].tag != nil {
59
-
dateI = self.refs[i].tag.Tagger.When
60
-
} else {
61
-
c, err := self.r.CommitObject(self.refs[i].ref.Hash())
62
-
if err != nil {
63
-
dateI = time.Now()
64
-
} else {
65
-
dateI = c.Committer.When
66
-
}
67
-
}
68
-
69
-
if self.refs[j].tag != nil {
70
-
dateJ = self.refs[j].tag.Tagger.When
71
-
} else {
72
-
c, err := self.r.CommitObject(self.refs[j].ref.Hash())
73
-
if err != nil {
74
-
dateJ = time.Now()
75
-
} else {
76
-
dateJ = c.Committer.When
77
-
}
78
-
}
79
-
80
-
return dateI.After(dateJ)
81
-
}
82
-
83
-
func Open(path string, ref string) (*GitRepo, error) {
84
-
var err error
85
-
g := GitRepo{}
86
-
g.r, err = git.PlainOpen(path)
87
-
if err != nil {
88
-
return nil, fmt.Errorf("opening %s: %w", path, err)
89
-
}
90
-
91
-
if ref == "" {
92
-
head, err := g.r.Head()
93
-
if err != nil {
94
-
return nil, fmt.Errorf("getting head of %s: %w", path, err)
95
-
}
96
-
g.h = head.Hash()
97
-
} else {
98
-
hash, err := g.r.ResolveRevision(plumbing.Revision(ref))
99
-
if err != nil {
100
-
return nil, fmt.Errorf("resolving rev %s for %s: %w", ref, path, err)
101
-
}
102
-
g.h = *hash
103
-
}
104
-
return &g, nil
105
-
}
106
-
107
-
func (g *GitRepo) Commits() ([]*object.Commit, error) {
108
-
ci, err := g.r.Log(&git.LogOptions{From: g.h})
109
-
if err != nil {
110
-
return nil, fmt.Errorf("commits from ref: %w", err)
111
-
}
112
-
113
-
commits := []*object.Commit{}
114
-
ci.ForEach(func(c *object.Commit) error {
115
-
commits = append(commits, c)
116
-
return nil
117
-
})
118
-
119
-
return commits, nil
120
-
}
121
-
122
-
func (g *GitRepo) LastCommit() (*object.Commit, error) {
123
-
c, err := g.r.CommitObject(g.h)
124
-
if err != nil {
125
-
return nil, fmt.Errorf("last commit: %w", err)
126
-
}
127
-
return c, nil
128
-
}
129
-
130
-
func (g *GitRepo) FileContent(path string) (string, error) {
131
-
c, err := g.r.CommitObject(g.h)
132
-
if err != nil {
133
-
return "", fmt.Errorf("commit object: %w", err)
134
-
}
135
-
136
-
tree, err := c.Tree()
137
-
if err != nil {
138
-
return "", fmt.Errorf("file tree: %w", err)
139
-
}
140
-
141
-
file, err := tree.File(path)
142
-
if err != nil {
143
-
return "", err
144
-
}
145
-
146
-
isbin, _ := file.IsBinary()
147
-
148
-
if !isbin {
149
-
return file.Contents()
150
-
} else {
151
-
return "Not displaying binary file", nil
152
-
}
153
-
}
154
-
155
-
func (g *GitRepo) Tags() ([]*TagReference, error) {
156
-
iter, err := g.r.Tags()
157
-
if err != nil {
158
-
return nil, fmt.Errorf("tag objects: %w", err)
159
-
}
160
-
161
-
tags := make([]*TagReference, 0)
162
-
163
-
if err := iter.ForEach(func(ref *plumbing.Reference) error {
164
-
obj, err := g.r.TagObject(ref.Hash())
165
-
switch err {
166
-
case nil:
167
-
tags = append(tags, &TagReference{
168
-
ref: ref,
169
-
tag: obj,
170
-
})
171
-
case plumbing.ErrObjectNotFound:
172
-
tags = append(tags, &TagReference{
173
-
ref: ref,
174
-
})
175
-
default:
176
-
return err
177
-
}
178
-
return nil
179
-
}); err != nil {
180
-
return nil, err
181
-
}
182
-
183
-
tagList := &TagList{r: g.r, refs: tags}
184
-
sort.Sort(tagList)
185
-
return tags, nil
186
-
}
187
-
188
-
func (g *GitRepo) Branches() ([]*plumbing.Reference, error) {
189
-
bi, err := g.r.Branches()
190
-
if err != nil {
191
-
return nil, fmt.Errorf("branchs: %w", err)
192
-
}
193
-
194
-
branches := []*plumbing.Reference{}
195
-
196
-
_ = bi.ForEach(func(ref *plumbing.Reference) error {
197
-
branches = append(branches, ref)
198
-
return nil
199
-
})
200
-
201
-
return branches, nil
202
-
}
203
-
204
-
func (g *GitRepo) FindMainBranch(branches []string) (string, error) {
205
-
206
-
for _, b := range branches {
207
-
_, err := g.r.ResolveRevision(plumbing.Revision(b))
208
-
if err == nil {
209
-
return b, nil
210
-
}
211
-
}
212
-
return "", fmt.Errorf("unable to find main branch")
213
-
}
214
-
215
-
// WriteTar writes itself from a tree into a binary tar file format.
216
-
// prefix is root folder to be appended.
217
-
func (g *GitRepo) WriteTar(w io.Writer, prefix string) error {
218
-
tw := tar.NewWriter(w)
219
-
defer tw.Close()
220
-
221
-
c, err := g.r.CommitObject(g.h)
222
-
if err != nil {
223
-
return fmt.Errorf("commit object: %w", err)
224
-
}
225
-
226
-
tree, err := c.Tree()
227
-
if err != nil {
228
-
return err
229
-
}
230
-
231
-
walker := object.NewTreeWalker(tree, true, nil)
232
-
defer walker.Close()
233
-
234
-
name, entry, err := walker.Next()
235
-
for ; err == nil; name, entry, err = walker.Next() {
236
-
info, err := newInfoWrapper(name, prefix, &entry, tree)
237
-
if err != nil {
238
-
return err
239
-
}
240
-
241
-
header, err := tar.FileInfoHeader(info, "")
242
-
if err != nil {
243
-
return err
244
-
}
245
-
246
-
err = tw.WriteHeader(header)
247
-
if err != nil {
248
-
return err
249
-
}
250
-
251
-
if !info.IsDir() {
252
-
file, err := tree.File(name)
253
-
if err != nil {
254
-
return err
255
-
}
256
-
257
-
reader, err := file.Blob.Reader()
258
-
if err != nil {
259
-
return err
260
-
}
261
-
262
-
_, err = io.Copy(tw, reader)
263
-
if err != nil {
264
-
reader.Close()
265
-
return err
266
-
}
267
-
reader.Close()
268
-
}
269
-
}
270
-
271
-
return nil
272
-
}
273
-
274
-
func newInfoWrapper(
275
-
name string,
276
-
prefix string,
277
-
entry *object.TreeEntry,
278
-
tree *object.Tree,
279
-
) (*infoWrapper, error) {
280
-
var (
281
-
size int64
282
-
mode fs.FileMode
283
-
isDir bool
284
-
)
285
-
286
-
if entry.Mode.IsFile() {
287
-
file, err := tree.TreeEntryFile(entry)
288
-
if err != nil {
289
-
return nil, err
290
-
}
291
-
mode = fs.FileMode(file.Mode)
292
-
293
-
size, err = tree.Size(name)
294
-
if err != nil {
295
-
return nil, err
296
-
}
297
-
} else {
298
-
isDir = true
299
-
mode = fs.ModeDir | fs.ModePerm
300
-
}
301
-
302
-
fullname := path.Join(prefix, name)
303
-
return &infoWrapper{
304
-
name: fullname,
305
-
size: size,
306
-
mode: mode,
307
-
modTime: time.Unix(0, 0),
308
-
isDir: isDir,
309
-
}, nil
310
-
}
311
-
312
-
func (i *infoWrapper) Name() string {
313
-
return i.name
314
-
}
315
-
316
-
func (i *infoWrapper) Size() int64 {
317
-
return i.size
318
-
}
319
-
320
-
func (i *infoWrapper) Mode() fs.FileMode {
321
-
return i.mode
322
-
}
323
-
324
-
func (i *infoWrapper) ModTime() time.Time {
325
-
return i.modTime
326
-
}
327
-
328
-
func (i *infoWrapper) IsDir() bool {
329
-
return i.isDir
330
-
}
331
-
332
-
func (i *infoWrapper) Sys() any {
333
-
return nil
334
-
}
335
-
336
-
func (t *TagReference) Name() string {
337
-
return t.ref.Name().Short()
338
-
}
339
-
340
-
func (t *TagReference) Message() string {
341
-
if t.tag != nil {
342
-
return t.tag.Message
343
-
}
344
-
return ""
345
-
}
-33
git/repo.go
-33
git/repo.go
···
1
-
package git
2
-
3
-
import (
4
-
"errors"
5
-
"fmt"
6
-
"os"
7
-
"path/filepath"
8
-
9
-
gogit "github.com/go-git/go-git/v5"
10
-
"github.com/go-git/go-git/v5/config"
11
-
)
12
-
13
-
func InitBare(path string) error {
14
-
parent := filepath.Dir(path)
15
-
16
-
if err := os.MkdirAll(parent, 0755); errors.Is(err, os.ErrExist) {
17
-
return fmt.Errorf("error creating user directory: %w", err)
18
-
}
19
-
20
-
repository, err := gogit.PlainInit(path, true)
21
-
if err != nil {
22
-
return err
23
-
}
24
-
25
-
err = repository.CreateBranch(&config.Branch{
26
-
Name: "main",
27
-
})
28
-
if err != nil {
29
-
return fmt.Errorf("creating branch: %w", err)
30
-
}
31
-
32
-
return nil
33
-
}
-121
git/service/service.go
-121
git/service/service.go
···
1
-
package service
2
-
3
-
import (
4
-
"bytes"
5
-
"fmt"
6
-
"io"
7
-
"log"
8
-
"net/http"
9
-
"os/exec"
10
-
"strings"
11
-
"syscall"
12
-
)
13
-
14
-
// Mostly from charmbracelet/soft-serve and sosedoff/gitkit.
15
-
16
-
type ServiceCommand struct {
17
-
Dir string
18
-
Stdin io.Reader
19
-
Stdout http.ResponseWriter
20
-
}
21
-
22
-
func (c *ServiceCommand) InfoRefs() error {
23
-
cmd := exec.Command("git", []string{
24
-
"upload-pack",
25
-
"--stateless-rpc",
26
-
"--advertise-refs",
27
-
".",
28
-
}...)
29
-
30
-
cmd.Dir = c.Dir
31
-
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
32
-
stdoutPipe, _ := cmd.StdoutPipe()
33
-
cmd.Stderr = cmd.Stdout
34
-
35
-
if err := cmd.Start(); err != nil {
36
-
log.Printf("git: failed to start git-upload-pack (info/refs): %s", err)
37
-
return err
38
-
}
39
-
40
-
if err := packLine(c.Stdout, "# service=git-upload-pack\n"); err != nil {
41
-
log.Printf("git: failed to write pack line: %s", err)
42
-
return err
43
-
}
44
-
45
-
if err := packFlush(c.Stdout); err != nil {
46
-
log.Printf("git: failed to flush pack: %s", err)
47
-
return err
48
-
}
49
-
50
-
buf := bytes.Buffer{}
51
-
if _, err := io.Copy(&buf, stdoutPipe); err != nil {
52
-
log.Printf("git: failed to copy stdout to tmp buffer: %s", err)
53
-
return err
54
-
}
55
-
56
-
if err := cmd.Wait(); err != nil {
57
-
out := strings.Builder{}
58
-
_, _ = io.Copy(&out, &buf)
59
-
log.Printf("git: failed to run git-upload-pack; err: %s; output: %s", err, out.String())
60
-
return err
61
-
}
62
-
63
-
if _, err := io.Copy(c.Stdout, &buf); err != nil {
64
-
log.Printf("git: failed to copy stdout: %s", err)
65
-
}
66
-
67
-
return nil
68
-
}
69
-
70
-
func (c *ServiceCommand) UploadPack() error {
71
-
cmd := exec.Command("git", []string{
72
-
"-c", "uploadpack.allowFilter=true",
73
-
"upload-pack",
74
-
"--stateless-rpc",
75
-
".",
76
-
}...)
77
-
cmd.Dir = c.Dir
78
-
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
79
-
80
-
stdoutPipe, _ := cmd.StdoutPipe()
81
-
cmd.Stderr = cmd.Stdout
82
-
defer stdoutPipe.Close()
83
-
84
-
stdinPipe, err := cmd.StdinPipe()
85
-
if err != nil {
86
-
return err
87
-
}
88
-
defer stdinPipe.Close()
89
-
90
-
if err := cmd.Start(); err != nil {
91
-
log.Printf("git: failed to start git-upload-pack: %s", err)
92
-
return err
93
-
}
94
-
95
-
if _, err := io.Copy(stdinPipe, c.Stdin); err != nil {
96
-
log.Printf("git: failed to copy stdin: %s", err)
97
-
return err
98
-
}
99
-
stdinPipe.Close()
100
-
101
-
if _, err := io.Copy(newWriteFlusher(c.Stdout), stdoutPipe); err != nil {
102
-
log.Printf("git: failed to copy stdout: %s", err)
103
-
return err
104
-
}
105
-
if err := cmd.Wait(); err != nil {
106
-
log.Printf("git: failed to wait for git-upload-pack: %s", err)
107
-
return err
108
-
}
109
-
110
-
return nil
111
-
}
112
-
113
-
func packLine(w io.Writer, s string) error {
114
-
_, err := fmt.Fprintf(w, "%04x%s", len(s)+4, s)
115
-
return err
116
-
}
117
-
118
-
func packFlush(w io.Writer) error {
119
-
_, err := fmt.Fprint(w, "0000")
120
-
return err
121
-
}
-25
git/service/write_flusher.go
-25
git/service/write_flusher.go
···
1
-
package service
2
-
3
-
import (
4
-
"io"
5
-
"net/http"
6
-
)
7
-
8
-
func newWriteFlusher(w http.ResponseWriter) io.Writer {
9
-
return writeFlusher{w.(interface {
10
-
io.Writer
11
-
http.Flusher
12
-
})}
13
-
}
14
-
15
-
type writeFlusher struct {
16
-
wf interface {
17
-
io.Writer
18
-
http.Flusher
19
-
}
20
-
}
21
-
22
-
func (w writeFlusher) Write(p []byte) (int, error) {
23
-
defer w.wf.Flush()
24
-
return w.wf.Write(p)
25
-
}
-66
git/tree.go
-66
git/tree.go
···
1
-
package git
2
-
3
-
import (
4
-
"fmt"
5
-
6
-
"github.com/go-git/go-git/v5/plumbing/object"
7
-
)
8
-
9
-
func (g *GitRepo) FileTree(path string) ([]NiceTree, error) {
10
-
c, err := g.r.CommitObject(g.h)
11
-
if err != nil {
12
-
return nil, fmt.Errorf("commit object: %w", err)
13
-
}
14
-
15
-
files := []NiceTree{}
16
-
tree, err := c.Tree()
17
-
if err != nil {
18
-
return nil, fmt.Errorf("file tree: %w", err)
19
-
}
20
-
21
-
if path == "" {
22
-
files = makeNiceTree(tree)
23
-
} else {
24
-
o, err := tree.FindEntry(path)
25
-
if err != nil {
26
-
return nil, err
27
-
}
28
-
29
-
if !o.Mode.IsFile() {
30
-
subtree, err := tree.Tree(path)
31
-
if err != nil {
32
-
return nil, err
33
-
}
34
-
35
-
files = makeNiceTree(subtree)
36
-
}
37
-
}
38
-
39
-
return files, nil
40
-
}
41
-
42
-
// A nicer git tree representation.
43
-
type NiceTree struct {
44
-
Name string
45
-
Mode string
46
-
Size int64
47
-
IsFile bool
48
-
IsSubtree bool
49
-
}
50
-
51
-
func makeNiceTree(t *object.Tree) []NiceTree {
52
-
nts := []NiceTree{}
53
-
54
-
for _, e := range t.Entries {
55
-
mode, _ := e.Mode.ToOSFileMode()
56
-
sz, _ := t.Size(e.Name)
57
-
nts = append(nts, NiceTree{
58
-
Name: e.Name,
59
-
Mode: mode.String(),
60
-
IsFile: e.Mode.IsFile(),
61
-
Size: sz,
62
-
})
63
-
}
64
-
65
-
return nts
66
-
}
+1
-1
knotserver/routes.go
+1
-1
knotserver/routes.go
···
19
19
"github.com/go-chi/chi/v5"
20
20
"github.com/go-git/go-git/v5/plumbing"
21
21
"github.com/go-git/go-git/v5/plumbing/object"
22
-
"github.com/icyphox/bild/db"
22
+
"github.com/icyphox/bild/knotserver/db"
23
23
"github.com/icyphox/bild/knotserver/git"
24
24
"github.com/russross/blackfriday/v2"
25
25
)
-35
routes/access.go
-35
routes/access.go
···
1
-
package routes
2
-
3
-
import (
4
-
"github.com/go-chi/chi/v5"
5
-
"github.com/icyphox/bild/auth"
6
-
"github.com/icyphox/bild/db"
7
-
"log"
8
-
"net/http"
9
-
)
10
-
11
-
func (h *Handle) AccessLevel(level db.Level) func(http.Handler) http.Handler {
12
-
return func(next http.Handler) http.Handler {
13
-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
14
-
repoOwnerHandle := chi.URLParam(r, "user")
15
-
repoOwner, err := auth.ResolveIdent(r.Context(), repoOwnerHandle)
16
-
if err != nil {
17
-
log.Println("invalid did")
18
-
http.Error(w, "invalid did", http.StatusNotFound)
19
-
return
20
-
}
21
-
repoName := chi.URLParam(r, "name")
22
-
session, _ := h.s.Get(r, "bild-session")
23
-
did := session.Values["did"].(string)
24
-
25
-
userLevel, err := h.db.GetAccessLevel(did, repoOwner.DID.String(), repoName)
26
-
if err != nil || userLevel < level {
27
-
log.Printf("unauthorized access: %s accessing %s/%s\n", did, repoOwnerHandle, repoName)
28
-
log.Printf("wanted level: %s, got level %s", level.String(), userLevel.String())
29
-
http.Error(w, "Forbidden", http.StatusUnauthorized)
30
-
return
31
-
}
32
-
next.ServeHTTP(w, r)
33
-
})
34
-
}
35
-
}
-72
routes/auth.go
-72
routes/auth.go
···
1
-
package routes
2
-
3
-
import (
4
-
"log"
5
-
"net/http"
6
-
"time"
7
-
8
-
comatproto "github.com/bluesky-social/indigo/api/atproto"
9
-
"github.com/bluesky-social/indigo/xrpc"
10
-
rauth "github.com/icyphox/bild/auth"
11
-
)
12
-
13
-
const (
14
-
layout = "2006-01-02 15:04:05.999999999 -0700 MST"
15
-
)
16
-
17
-
func (h *Handle) AuthMiddleware(next http.Handler) http.Handler {
18
-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19
-
session, _ := h.s.Get(r, "bild-session")
20
-
auth, ok := session.Values["authenticated"].(bool)
21
-
22
-
if !ok || !auth {
23
-
log.Printf("not logged in, redirecting")
24
-
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
25
-
return
26
-
}
27
-
28
-
// refresh if nearing expiry
29
-
// TODO: dedup with /login
30
-
expiryStr := session.Values["expiry"].(string)
31
-
expiry, _ := time.Parse(layout, expiryStr)
32
-
pdsUrl := session.Values["pds"].(string)
33
-
did := session.Values["did"].(string)
34
-
refreshJwt := session.Values["refreshJwt"].(string)
35
-
36
-
if time.Now().After((expiry)) {
37
-
log.Println("token expired, refreshing ...")
38
-
39
-
client := xrpc.Client{
40
-
Host: pdsUrl,
41
-
Auth: &xrpc.AuthInfo{
42
-
Did: did,
43
-
AccessJwt: refreshJwt,
44
-
RefreshJwt: refreshJwt,
45
-
},
46
-
}
47
-
atSession, err := comatproto.ServerRefreshSession(r.Context(), &client)
48
-
if err != nil {
49
-
log.Println(err)
50
-
h.Write500(w)
51
-
return
52
-
}
53
-
54
-
err = h.auth.StoreSession(r, w, nil, &rauth.AtSessionRefresh{ServerRefreshSession_Output: *atSession, PDSEndpoint: pdsUrl})
55
-
if err != nil {
56
-
log.Printf("failed to store session for did: %s\n: %s", atSession.Did, err)
57
-
h.Write500(w)
58
-
return
59
-
}
60
-
61
-
log.Println("successfully refreshed token")
62
-
}
63
-
64
-
if r.URL.Path == "/login" {
65
-
log.Println("already logged in")
66
-
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
67
-
return
68
-
}
69
-
70
-
next.ServeHTTP(w, r)
71
-
})
72
-
}
-122
routes/file.go
-122
routes/file.go
···
1
-
package routes
2
-
3
-
import (
4
-
"bytes"
5
-
"html/template"
6
-
"io"
7
-
"log"
8
-
"net/http"
9
-
"strings"
10
-
11
-
"github.com/alecthomas/chroma/v2/formatters/html"
12
-
"github.com/alecthomas/chroma/v2/lexers"
13
-
"github.com/alecthomas/chroma/v2/styles"
14
-
"github.com/icyphox/bild/git"
15
-
)
16
-
17
-
func (h *Handle) listFiles(files []git.NiceTree, data map[string]any, w http.ResponseWriter) {
18
-
data["files"] = files
19
-
data["meta"] = h.c.Meta
20
-
21
-
if err := h.t.ExecuteTemplate(w, "repo/tree", data); err != nil {
22
-
log.Println(err)
23
-
return
24
-
}
25
-
}
26
-
27
-
func countLines(r io.Reader) (int, error) {
28
-
buf := make([]byte, 32*1024)
29
-
bufLen := 0
30
-
count := 0
31
-
nl := []byte{'\n'}
32
-
33
-
for {
34
-
c, err := r.Read(buf)
35
-
if c > 0 {
36
-
bufLen += c
37
-
}
38
-
count += bytes.Count(buf[:c], nl)
39
-
40
-
switch {
41
-
case err == io.EOF:
42
-
/* handle last line not having a newline at the end */
43
-
if bufLen >= 1 && buf[(bufLen-1)%(32*1024)] != '\n' {
44
-
count++
45
-
}
46
-
return count, nil
47
-
case err != nil:
48
-
return 0, err
49
-
}
50
-
}
51
-
}
52
-
53
-
func (h *Handle) showFileWithHighlight(name, content string, data map[string]any, w http.ResponseWriter) {
54
-
lexer := lexers.Get(name)
55
-
if lexer == nil {
56
-
lexer = lexers.Get(".txt")
57
-
}
58
-
59
-
style := styles.Get(h.c.Meta.SyntaxHighlight)
60
-
if style == nil {
61
-
style = styles.Get("monokailight")
62
-
}
63
-
64
-
formatter := html.New(
65
-
html.WithLineNumbers(true),
66
-
html.WithLinkableLineNumbers(true, "L"),
67
-
)
68
-
69
-
iterator, err := lexer.Tokenise(nil, content)
70
-
if err != nil {
71
-
h.Write500(w)
72
-
return
73
-
}
74
-
75
-
var code bytes.Buffer
76
-
err = formatter.Format(&code, style, iterator)
77
-
if err != nil {
78
-
h.Write500(w)
79
-
return
80
-
}
81
-
82
-
data["content"] = template.HTML(code.String())
83
-
data["meta"] = h.c.Meta
84
-
data["chroma"] = true
85
-
86
-
if err := h.t.ExecuteTemplate(w, "repo/file", data); err != nil {
87
-
log.Println(err)
88
-
return
89
-
}
90
-
}
91
-
92
-
func (h *Handle) showFile(content string, data map[string]any, w http.ResponseWriter) {
93
-
lc, err := countLines(strings.NewReader(content))
94
-
if err != nil {
95
-
// Non-fatal, we'll just skip showing line numbers in the template.
96
-
log.Printf("counting lines: %s", err)
97
-
}
98
-
99
-
lines := make([]int, lc)
100
-
if lc > 0 {
101
-
for i := range lines {
102
-
lines[i] = i + 1
103
-
}
104
-
}
105
-
106
-
data["linecount"] = lines
107
-
data["content"] = content
108
-
data["meta"] = h.c.Meta
109
-
data["chroma"] = false
110
-
111
-
if err := h.t.ExecuteTemplate(w, "repo/file", data); err != nil {
112
-
log.Println(err)
113
-
return
114
-
}
115
-
}
116
-
117
-
func (h *Handle) showRaw(content string, w http.ResponseWriter) {
118
-
w.WriteHeader(http.StatusOK)
119
-
w.Header().Set("Content-Type", "text/plain")
120
-
w.Write([]byte(content))
121
-
return
122
-
}
-69
routes/git.go
-69
routes/git.go
···
1
-
package routes
2
-
3
-
import (
4
-
"compress/gzip"
5
-
"io"
6
-
"log"
7
-
"net/http"
8
-
"path/filepath"
9
-
10
-
"github.com/icyphox/bild/git/service"
11
-
)
12
-
13
-
func (d *Handle) InfoRefs(w http.ResponseWriter, r *http.Request) {
14
-
name := displayRepoName(r)
15
-
name = filepath.Clean(name)
16
-
17
-
repo := filepath.Join(d.c.Repo.ScanPath, name)
18
-
19
-
w.Header().Set("content-type", "application/x-git-upload-pack-advertisement")
20
-
w.WriteHeader(http.StatusOK)
21
-
22
-
cmd := service.ServiceCommand{
23
-
Dir: repo,
24
-
Stdout: w,
25
-
}
26
-
27
-
if err := cmd.InfoRefs(); err != nil {
28
-
http.Error(w, err.Error(), 500)
29
-
log.Printf("git: failed to execute git-upload-pack (info/refs) %s", err)
30
-
return
31
-
}
32
-
}
33
-
34
-
func (d *Handle) UploadPack(w http.ResponseWriter, r *http.Request) {
35
-
name := displayRepoName(r)
36
-
name = filepath.Clean(name)
37
-
38
-
repo := filepath.Join(d.c.Repo.ScanPath, name)
39
-
40
-
w.Header().Set("content-type", "application/x-git-upload-pack-result")
41
-
w.Header().Set("Connection", "Keep-Alive")
42
-
w.Header().Set("Transfer-Encoding", "chunked")
43
-
w.WriteHeader(http.StatusOK)
44
-
45
-
cmd := service.ServiceCommand{
46
-
Dir: repo,
47
-
Stdout: w,
48
-
}
49
-
50
-
var reader io.ReadCloser
51
-
reader = r.Body
52
-
53
-
if r.Header.Get("Content-Encoding") == "gzip" {
54
-
reader, err := gzip.NewReader(r.Body)
55
-
if err != nil {
56
-
http.Error(w, err.Error(), 500)
57
-
log.Printf("git: failed to create gzip reader: %s", err)
58
-
return
59
-
}
60
-
defer reader.Close()
61
-
}
62
-
63
-
cmd.Stdin = reader
64
-
if err := cmd.UploadPack(); err != nil {
65
-
http.Error(w, err.Error(), 500)
66
-
log.Printf("git: failed to execute git-upload-pack %s", err)
67
-
return
68
-
}
69
-
}
-118
routes/handler.go
-118
routes/handler.go
···
1
-
package routes
2
-
3
-
import (
4
-
"fmt"
5
-
"net/http"
6
-
7
-
_ "github.com/bluesky-social/indigo/atproto/identity"
8
-
_ "github.com/bluesky-social/indigo/atproto/syntax"
9
-
_ "github.com/bluesky-social/indigo/xrpc"
10
-
"github.com/go-chi/chi/v5"
11
-
"github.com/gorilla/sessions"
12
-
"github.com/icyphox/bild/auth"
13
-
"github.com/icyphox/bild/config"
14
-
database "github.com/icyphox/bild/db"
15
-
"github.com/icyphox/bild/routes/middleware"
16
-
"github.com/icyphox/bild/routes/tmpl"
17
-
)
18
-
19
-
// Checks for gitprotocol-http(5) specific smells; if found, passes
20
-
// the request on to the git http service, else render the web frontend.
21
-
func (h *Handle) Multiplex(w http.ResponseWriter, r *http.Request) {
22
-
path := chi.URLParam(r, "*")
23
-
24
-
if r.URL.RawQuery == "service=git-receive-pack" {
25
-
w.WriteHeader(http.StatusBadRequest)
26
-
w.Write([]byte("no pushing allowed!"))
27
-
return
28
-
}
29
-
30
-
if path == "info/refs" &&
31
-
r.URL.RawQuery == "service=git-upload-pack" &&
32
-
r.Method == "GET" {
33
-
h.InfoRefs(w, r)
34
-
} else if path == "git-upload-pack" && r.Method == "POST" {
35
-
h.UploadPack(w, r)
36
-
} else if r.Method == "GET" {
37
-
h.RepoIndex(w, r)
38
-
}
39
-
}
40
-
41
-
func Setup(c *config.Config, db *database.DB) (http.Handler, error) {
42
-
r := chi.NewRouter()
43
-
s := sessions.NewCookieStore([]byte("TODO_CHANGE_ME"))
44
-
t, err := tmpl.Load(c.Dirs.Templates)
45
-
if err != nil {
46
-
return nil, fmt.Errorf("failed to load templates: %w", err)
47
-
}
48
-
49
-
auth := auth.NewAuth(s)
50
-
51
-
h := Handle{
52
-
c: c,
53
-
t: t,
54
-
s: s,
55
-
db: db,
56
-
auth: auth,
57
-
}
58
-
59
-
r.Get("/", h.Timeline)
60
-
61
-
r.Group(func(r chi.Router) {
62
-
r.Get("/login", h.Login)
63
-
r.Post("/login", h.Login)
64
-
})
65
-
r.Get("/static/{file}", h.ServeStatic)
66
-
67
-
r.Route("/repo", func(r chi.Router) {
68
-
r.Use(h.AuthMiddleware)
69
-
r.Get("/new", h.NewRepo)
70
-
r.Put("/new", h.NewRepo)
71
-
})
72
-
73
-
r.Group(func(r chi.Router) {
74
-
r.Use(h.AuthMiddleware)
75
-
r.Route("/settings", func(r chi.Router) {
76
-
r.Get("/keys", h.Keys)
77
-
r.Put("/keys", h.Keys)
78
-
})
79
-
})
80
-
81
-
r.Route("/@{user}", func(r chi.Router) {
82
-
r.Use(middleware.AddDID)
83
-
r.Get("/", h.Index)
84
-
85
-
// Repo routes
86
-
r.Route("/{name}", func(r chi.Router) {
87
-
r.Get("/", h.Multiplex)
88
-
r.Post("/", h.Multiplex)
89
-
90
-
r.Route("/tree/{ref}", func(r chi.Router) {
91
-
r.Get("/*", h.RepoTree)
92
-
})
93
-
94
-
r.Route("/blob/{ref}", func(r chi.Router) {
95
-
r.Get("/*", h.FileContent)
96
-
})
97
-
98
-
r.Get("/log/{ref}", h.Log)
99
-
r.Get("/archive/{file}", h.Archive)
100
-
r.Get("/commit/{ref}", h.Diff)
101
-
r.Get("/refs/", h.Refs)
102
-
103
-
r.Group(func(r chi.Router) {
104
-
// settings page is only accessible to owners
105
-
r.Use(h.AccessLevel(database.Owner))
106
-
r.Route("/settings", func(r chi.Router) {
107
-
r.Put("/collaborators", h.Collaborators)
108
-
})
109
-
})
110
-
111
-
// Catch-all routes
112
-
r.Get("/*", h.Multiplex)
113
-
r.Post("/*", h.Multiplex)
114
-
})
115
-
})
116
-
117
-
return r, nil
118
-
}
-29
routes/html_util.go
-29
routes/html_util.go
···
1
-
package routes
2
-
3
-
import (
4
-
"fmt"
5
-
"log"
6
-
"net/http"
7
-
)
8
-
9
-
func (h *Handle) Write404(w http.ResponseWriter) {
10
-
w.WriteHeader(404)
11
-
if err := h.t.ExecuteTemplate(w, "errors/404", nil); err != nil {
12
-
log.Printf("404 template: %s", err)
13
-
}
14
-
}
15
-
16
-
func (h *Handle) Write500(w http.ResponseWriter) {
17
-
w.WriteHeader(500)
18
-
if err := h.t.ExecuteTemplate(w, "errors/500", nil); err != nil {
19
-
log.Printf("500 template: %s", err)
20
-
}
21
-
}
22
-
23
-
func (h *Handle) WriteOOBNotice(w http.ResponseWriter, id, msg string) {
24
-
html := fmt.Sprintf(`<span id="%s" hx-swap-oob="innerHTML">%s</span>`, id, msg)
25
-
26
-
w.Header().Set("Content-Type", "text/html")
27
-
w.WriteHeader(http.StatusOK)
28
-
w.Write([]byte(html))
29
-
}
-62
routes/internal.go
-62
routes/internal.go
···
1
-
package routes
2
-
3
-
import (
4
-
"encoding/json"
5
-
"net/http"
6
-
7
-
"github.com/go-chi/chi/v5"
8
-
"github.com/icyphox/bild/config"
9
-
"github.com/icyphox/bild/db"
10
-
)
11
-
12
-
type InternalHandle struct {
13
-
c *config.Config
14
-
db *db.DB
15
-
}
16
-
17
-
func SetupInternal(c *config.Config, db *db.DB) http.Handler {
18
-
ih := &InternalHandle{
19
-
c: c,
20
-
db: db,
21
-
}
22
-
23
-
r := chi.NewRouter()
24
-
r.Route("/internal/allkeys", func(r chi.Router) {
25
-
r.Get("/", ih.AllKeys)
26
-
})
27
-
28
-
return r
29
-
}
30
-
31
-
func (h *InternalHandle) returnJSON(w http.ResponseWriter, data interface{}) error {
32
-
w.Header().Set("Content-Type", "application/json")
33
-
res, err := json.Marshal(data)
34
-
if err != nil {
35
-
return err
36
-
}
37
-
_, err = w.Write(res)
38
-
return err
39
-
}
40
-
41
-
func (h *InternalHandle) returnErr(w http.ResponseWriter, err error) error {
42
-
w.WriteHeader(http.StatusInternalServerError)
43
-
return h.returnJSON(w, map[string]string{
44
-
"error": err.Error(),
45
-
})
46
-
}
47
-
48
-
func (h *InternalHandle) AllKeys(w http.ResponseWriter, r *http.Request) {
49
-
keys, err := h.db.GetAllPublicKeys()
50
-
if err != nil {
51
-
h.returnErr(w, err)
52
-
return
53
-
}
54
-
keyMap := map[string]string{}
55
-
for _, key := range keys {
56
-
keyMap[key.DID] = key.Key
57
-
}
58
-
if err := h.returnJSON(w, keyMap); err != nil {
59
-
h.returnErr(w, err)
60
-
return
61
-
}
62
-
}
-61
routes/middleware/did.go
-61
routes/middleware/did.go
···
1
-
package middleware
2
-
3
-
import (
4
-
"context"
5
-
"log"
6
-
"net/http"
7
-
"sync"
8
-
"time"
9
-
10
-
"github.com/bluesky-social/indigo/atproto/identity"
11
-
"github.com/icyphox/bild/auth"
12
-
)
13
-
14
-
type cachedIdent struct {
15
-
ident *identity.Identity
16
-
expiry time.Time
17
-
}
18
-
19
-
var (
20
-
identCache = make(map[string]cachedIdent)
21
-
cacheMutex sync.RWMutex
22
-
)
23
-
24
-
// Only use this middleware for routes that require a handle
25
-
// /@{user}/...
26
-
func AddDID(next http.Handler) http.Handler {
27
-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
28
-
user := r.PathValue("user")
29
-
30
-
// Check cache first
31
-
cacheMutex.RLock()
32
-
if cached, ok := identCache[user]; ok && time.Now().Before(cached.expiry) {
33
-
cacheMutex.RUnlock()
34
-
ctx := context.WithValue(r.Context(), "did", cached.ident.DID.String())
35
-
r = r.WithContext(ctx)
36
-
next.ServeHTTP(w, r)
37
-
return
38
-
}
39
-
cacheMutex.RUnlock()
40
-
41
-
// Cache miss - resolve and cache
42
-
ident, err := auth.ResolveIdent(r.Context(), user)
43
-
if err != nil {
44
-
log.Println("error resolving identity", err)
45
-
http.Error(w, "error resolving identity", http.StatusNotFound)
46
-
return
47
-
}
48
-
49
-
cacheMutex.Lock()
50
-
identCache[user] = cachedIdent{
51
-
ident: ident,
52
-
expiry: time.Now().Add(24 * time.Hour),
53
-
}
54
-
cacheMutex.Unlock()
55
-
56
-
ctx := context.WithValue(r.Context(), "did", ident.DID.String())
57
-
r = r.WithContext(ctx)
58
-
59
-
next.ServeHTTP(w, r)
60
-
})
61
-
}
-643
routes/routes.go
-643
routes/routes.go
···
1
-
package routes
2
-
3
-
import (
4
-
"compress/gzip"
5
-
"errors"
6
-
"fmt"
7
-
"html/template"
8
-
"log"
9
-
"net/http"
10
-
"os"
11
-
"path/filepath"
12
-
"sort"
13
-
"strconv"
14
-
"strings"
15
-
"time"
16
-
17
-
comatproto "github.com/bluesky-social/indigo/api/atproto"
18
-
lexutil "github.com/bluesky-social/indigo/lex/util"
19
-
"github.com/dustin/go-humanize"
20
-
"github.com/go-chi/chi/v5"
21
-
"github.com/go-git/go-git/v5/plumbing"
22
-
"github.com/google/uuid"
23
-
"github.com/gorilla/sessions"
24
-
shbild "github.com/icyphox/bild/api/bild"
25
-
"github.com/icyphox/bild/auth"
26
-
"github.com/icyphox/bild/config"
27
-
"github.com/icyphox/bild/db"
28
-
"github.com/icyphox/bild/git"
29
-
"github.com/russross/blackfriday/v2"
30
-
"golang.org/x/crypto/ssh"
31
-
)
32
-
33
-
type Handle struct {
34
-
c *config.Config
35
-
t *template.Template
36
-
s *sessions.CookieStore
37
-
db *db.DB
38
-
auth *auth.Auth
39
-
}
40
-
41
-
func (h *Handle) Index(w http.ResponseWriter, r *http.Request) {
42
-
name := displayRepoName(r)
43
-
path := filepath.Join(h.c.Repo.ScanPath, name)
44
-
dirs, err := os.ReadDir(path)
45
-
if err != nil {
46
-
h.Write500(w)
47
-
log.Printf("reading scan path: %s", err)
48
-
return
49
-
}
50
-
51
-
type info struct {
52
-
DisplayName, Name, Desc, Idle string
53
-
d time.Time
54
-
}
55
-
56
-
infos := []info{}
57
-
58
-
for _, dir := range dirs {
59
-
name := dir.Name()
60
-
if !dir.IsDir() || h.isIgnored(name) || h.isUnlisted(name) {
61
-
continue
62
-
}
63
-
64
-
gr, err := git.Open(path, "")
65
-
if err != nil {
66
-
log.Println(err)
67
-
continue
68
-
}
69
-
70
-
c, err := gr.LastCommit()
71
-
if err != nil {
72
-
h.Write500(w)
73
-
log.Println(err)
74
-
return
75
-
}
76
-
77
-
infos = append(infos, info{
78
-
DisplayName: trimDotGit(name),
79
-
Name: name,
80
-
Desc: getDescription(path),
81
-
Idle: humanize.Time(c.Author.When),
82
-
d: c.Author.When,
83
-
})
84
-
}
85
-
86
-
sort.Slice(infos, func(i, j int) bool {
87
-
return infos[j].d.Before(infos[i].d)
88
-
})
89
-
90
-
data := make(map[string]interface{})
91
-
data["meta"] = h.c.Meta
92
-
data["info"] = infos
93
-
94
-
if err := h.t.ExecuteTemplate(w, "index", data); err != nil {
95
-
log.Println(err)
96
-
return
97
-
}
98
-
}
99
-
100
-
func (h *Handle) RepoIndex(w http.ResponseWriter, r *http.Request) {
101
-
name := displayRepoName(r)
102
-
if h.isIgnored(name) {
103
-
h.Write404(w)
104
-
return
105
-
}
106
-
107
-
path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
108
-
109
-
gr, err := git.Open(path, "")
110
-
if err != nil {
111
-
if errors.Is(err, plumbing.ErrReferenceNotFound) {
112
-
h.t.ExecuteTemplate(w, "repo/empty", nil)
113
-
return
114
-
} else {
115
-
h.Write404(w)
116
-
return
117
-
}
118
-
}
119
-
commits, err := gr.Commits()
120
-
if err != nil {
121
-
h.Write500(w)
122
-
log.Println(err)
123
-
return
124
-
}
125
-
126
-
var readmeContent template.HTML
127
-
for _, readme := range h.c.Repo.Readme {
128
-
ext := filepath.Ext(readme)
129
-
content, _ := gr.FileContent(readme)
130
-
if len(content) > 0 {
131
-
switch ext {
132
-
case ".md", ".mkd", ".markdown":
133
-
unsafe := blackfriday.Run(
134
-
[]byte(content),
135
-
blackfriday.WithExtensions(blackfriday.CommonExtensions),
136
-
)
137
-
html := sanitize(unsafe)
138
-
readmeContent = template.HTML(html)
139
-
default:
140
-
safe := sanitize([]byte(content))
141
-
readmeContent = template.HTML(
142
-
fmt.Sprintf(`<pre>%s</pre>`, safe),
143
-
)
144
-
}
145
-
break
146
-
}
147
-
}
148
-
149
-
if readmeContent == "" {
150
-
log.Printf("no readme found for %s", name)
151
-
}
152
-
153
-
mainBranch, err := gr.FindMainBranch(h.c.Repo.MainBranch)
154
-
if err != nil {
155
-
h.Write500(w)
156
-
log.Println(err)
157
-
return
158
-
}
159
-
160
-
if len(commits) >= 3 {
161
-
commits = commits[:3]
162
-
}
163
-
164
-
data := make(map[string]any)
165
-
data["name"] = name
166
-
data["displayname"] = trimDotGit(name)
167
-
data["ref"] = mainBranch
168
-
data["readme"] = readmeContent
169
-
data["commits"] = commits
170
-
data["desc"] = getDescription(path)
171
-
data["servername"] = h.c.Server.Name
172
-
data["meta"] = h.c.Meta
173
-
data["gomod"] = isGoModule(gr)
174
-
175
-
if err := h.t.ExecuteTemplate(w, "repo/repo", data); err != nil {
176
-
log.Println(err)
177
-
return
178
-
}
179
-
180
-
return
181
-
}
182
-
183
-
func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) {
184
-
name := displayRepoName(r)
185
-
if h.isIgnored(name) {
186
-
h.Write404(w)
187
-
return
188
-
}
189
-
treePath := chi.URLParam(r, "*")
190
-
ref := chi.URLParam(r, "ref")
191
-
192
-
path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
193
-
gr, err := git.Open(path, ref)
194
-
if err != nil {
195
-
h.Write404(w)
196
-
return
197
-
}
198
-
199
-
files, err := gr.FileTree(treePath)
200
-
if err != nil {
201
-
h.Write500(w)
202
-
log.Println(err)
203
-
return
204
-
}
205
-
206
-
data := make(map[string]any)
207
-
data["name"] = name
208
-
data["displayname"] = trimDotGit(name)
209
-
data["ref"] = ref
210
-
data["parent"] = treePath
211
-
data["desc"] = getDescription(path)
212
-
data["dotdot"] = filepath.Dir(treePath)
213
-
214
-
h.listFiles(files, data, w)
215
-
return
216
-
}
217
-
218
-
func (h *Handle) FileContent(w http.ResponseWriter, r *http.Request) {
219
-
var raw bool
220
-
if rawParam, err := strconv.ParseBool(r.URL.Query().Get("raw")); err == nil {
221
-
raw = rawParam
222
-
}
223
-
224
-
name := displayRepoName(r)
225
-
226
-
if h.isIgnored(name) {
227
-
h.Write404(w)
228
-
return
229
-
}
230
-
treePath := chi.URLParam(r, "*")
231
-
ref := chi.URLParam(r, "ref")
232
-
233
-
path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
234
-
gr, err := git.Open(path, ref)
235
-
if err != nil {
236
-
h.Write404(w)
237
-
return
238
-
}
239
-
240
-
contents, err := gr.FileContent(treePath)
241
-
if err != nil {
242
-
h.Write500(w)
243
-
return
244
-
}
245
-
data := make(map[string]any)
246
-
data["name"] = name
247
-
data["displayname"] = trimDotGit(name)
248
-
data["ref"] = ref
249
-
data["desc"] = getDescription(path)
250
-
data["path"] = treePath
251
-
252
-
safe := sanitize([]byte(contents))
253
-
254
-
if raw {
255
-
h.showRaw(string(safe), w)
256
-
} else {
257
-
if h.c.Meta.SyntaxHighlight == "" {
258
-
h.showFile(string(safe), data, w)
259
-
} else {
260
-
h.showFileWithHighlight(treePath, string(safe), data, w)
261
-
}
262
-
}
263
-
}
264
-
265
-
func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) {
266
-
name := displayRepoName(r)
267
-
if h.isIgnored(name) {
268
-
h.Write404(w)
269
-
return
270
-
}
271
-
272
-
file := chi.URLParam(r, "file")
273
-
274
-
// TODO: extend this to add more files compression (e.g.: xz)
275
-
if !strings.HasSuffix(file, ".tar.gz") {
276
-
h.Write404(w)
277
-
return
278
-
}
279
-
280
-
ref := strings.TrimSuffix(file, ".tar.gz")
281
-
282
-
// This allows the browser to use a proper name for the file when
283
-
// downloading
284
-
filename := fmt.Sprintf("%s-%s.tar.gz", name, ref)
285
-
setContentDisposition(w, filename)
286
-
setGZipMIME(w)
287
-
288
-
path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
289
-
gr, err := git.Open(path, ref)
290
-
if err != nil {
291
-
h.Write404(w)
292
-
return
293
-
}
294
-
295
-
gw := gzip.NewWriter(w)
296
-
defer gw.Close()
297
-
298
-
prefix := fmt.Sprintf("%s-%s", name, ref)
299
-
err = gr.WriteTar(gw, prefix)
300
-
if err != nil {
301
-
// once we start writing to the body we can't report error anymore
302
-
// so we are only left with printing the error.
303
-
log.Println(err)
304
-
return
305
-
}
306
-
307
-
err = gw.Flush()
308
-
if err != nil {
309
-
// once we start writing to the body we can't report error anymore
310
-
// so we are only left with printing the error.
311
-
log.Println(err)
312
-
return
313
-
}
314
-
}
315
-
316
-
func (h *Handle) Log(w http.ResponseWriter, r *http.Request) {
317
-
name := displayRepoName(r)
318
-
if h.isIgnored(name) {
319
-
h.Write404(w)
320
-
return
321
-
}
322
-
ref := chi.URLParam(r, "ref")
323
-
324
-
path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
325
-
gr, err := git.Open(path, ref)
326
-
if err != nil {
327
-
h.Write404(w)
328
-
return
329
-
}
330
-
331
-
commits, err := gr.Commits()
332
-
if err != nil {
333
-
h.Write500(w)
334
-
log.Println(err)
335
-
return
336
-
}
337
-
338
-
data := make(map[string]interface{})
339
-
data["commits"] = commits
340
-
data["meta"] = h.c.Meta
341
-
data["name"] = name
342
-
data["displayname"] = trimDotGit(name)
343
-
data["ref"] = ref
344
-
data["desc"] = getDescription(path)
345
-
data["log"] = true
346
-
347
-
if err := h.t.ExecuteTemplate(w, "repo/log", data); err != nil {
348
-
log.Println(err)
349
-
return
350
-
}
351
-
}
352
-
353
-
func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) {
354
-
name := displayRepoName(r)
355
-
if h.isIgnored(name) {
356
-
h.Write404(w)
357
-
return
358
-
}
359
-
ref := chi.URLParam(r, "ref")
360
-
361
-
path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
362
-
gr, err := git.Open(path, ref)
363
-
if err != nil {
364
-
h.Write404(w)
365
-
return
366
-
}
367
-
368
-
diff, err := gr.Diff()
369
-
if err != nil {
370
-
h.Write500(w)
371
-
log.Println(err)
372
-
return
373
-
}
374
-
375
-
data := make(map[string]interface{})
376
-
377
-
data["commit"] = diff.Commit
378
-
data["stat"] = diff.Stat
379
-
data["diff"] = diff.Diff
380
-
data["meta"] = h.c.Meta
381
-
data["name"] = name
382
-
data["displayname"] = trimDotGit(name)
383
-
data["ref"] = ref
384
-
data["desc"] = getDescription(path)
385
-
386
-
if err := h.t.ExecuteTemplate(w, "repo/commit", data); err != nil {
387
-
log.Println(err)
388
-
return
389
-
}
390
-
}
391
-
392
-
func (h *Handle) Refs(w http.ResponseWriter, r *http.Request) {
393
-
name := chi.URLParam(r, "name")
394
-
if h.isIgnored(name) {
395
-
h.Write404(w)
396
-
return
397
-
}
398
-
399
-
path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
400
-
gr, err := git.Open(path, "")
401
-
if err != nil {
402
-
h.Write404(w)
403
-
return
404
-
}
405
-
406
-
tags, err := gr.Tags()
407
-
if err != nil {
408
-
// Non-fatal, we *should* have at least one branch to show.
409
-
log.Println(err)
410
-
}
411
-
412
-
branches, err := gr.Branches()
413
-
if err != nil {
414
-
log.Println(err)
415
-
h.Write500(w)
416
-
return
417
-
}
418
-
419
-
data := make(map[string]interface{})
420
-
421
-
data["meta"] = h.c.Meta
422
-
data["name"] = name
423
-
data["displayname"] = trimDotGit(name)
424
-
data["branches"] = branches
425
-
data["tags"] = tags
426
-
data["desc"] = getDescription(path)
427
-
428
-
if err := h.t.ExecuteTemplate(w, "repo/refs", data); err != nil {
429
-
log.Println(err)
430
-
return
431
-
}
432
-
}
433
-
434
-
func (h *Handle) Collaborators(w http.ResponseWriter, r *http.Request) {
435
-
// put repo resolution in middleware
436
-
repoOwnerHandle := chi.URLParam(r, "user")
437
-
repoOwner, err := auth.ResolveIdent(r.Context(), repoOwnerHandle)
438
-
if err != nil {
439
-
log.Println("invalid did")
440
-
http.Error(w, "invalid did", http.StatusNotFound)
441
-
return
442
-
}
443
-
repoName := chi.URLParam(r, "name")
444
-
445
-
switch r.Method {
446
-
case http.MethodGet:
447
-
// TODO fetch a list of collaborators and their access rights
448
-
http.Error(w, "unimpl 1", http.StatusInternalServerError)
449
-
return
450
-
case http.MethodPut:
451
-
newUser := r.FormValue("newUser")
452
-
if newUser == "" {
453
-
// TODO: htmx this
454
-
http.Error(w, "unimpl 2", http.StatusInternalServerError)
455
-
return
456
-
}
457
-
newUserIdentity, err := auth.ResolveIdent(r.Context(), newUser)
458
-
if err != nil {
459
-
// TODO: htmx this
460
-
log.Println("invalid handle")
461
-
http.Error(w, "unimpl 3", http.StatusBadRequest)
462
-
return
463
-
}
464
-
err = h.db.SetWriter(newUserIdentity.DID.String(), repoOwner.DID.String(), repoName)
465
-
if err != nil {
466
-
// TODO: htmx this
467
-
log.Println("failed to add collaborator")
468
-
http.Error(w, "unimpl 4", http.StatusInternalServerError)
469
-
return
470
-
}
471
-
472
-
log.Println("success")
473
-
return
474
-
475
-
}
476
-
}
477
-
478
-
func (h *Handle) ServeStatic(w http.ResponseWriter, r *http.Request) {
479
-
f := chi.URLParam(r, "file")
480
-
f = filepath.Clean(filepath.Join(h.c.Dirs.Static, f))
481
-
482
-
http.ServeFile(w, r, f)
483
-
}
484
-
485
-
func (h *Handle) Login(w http.ResponseWriter, r *http.Request) {
486
-
switch r.Method {
487
-
case http.MethodGet:
488
-
if err := h.t.ExecuteTemplate(w, "user/login", nil); err != nil {
489
-
log.Println(err)
490
-
return
491
-
}
492
-
case http.MethodPost:
493
-
username := r.FormValue("username")
494
-
appPassword := r.FormValue("app_password")
495
-
496
-
atSession, err := h.auth.CreateInitialSession(w, r, username, appPassword)
497
-
if err != nil {
498
-
h.WriteOOBNotice(w, "login", "Invalid username or app password.")
499
-
log.Printf("creating initial session: %s", err)
500
-
return
501
-
}
502
-
503
-
err = h.auth.StoreSession(r, w, &atSession, nil)
504
-
if err != nil {
505
-
h.WriteOOBNotice(w, "login", "Failed to store session.")
506
-
log.Printf("storing session: %s", err)
507
-
return
508
-
}
509
-
510
-
log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did)
511
-
http.Redirect(w, r, "/", http.StatusSeeOther)
512
-
return
513
-
}
514
-
}
515
-
516
-
func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) {
517
-
session, _ := h.s.Get(r, "bild-session")
518
-
did := session.Values["did"].(string)
519
-
520
-
switch r.Method {
521
-
case http.MethodGet:
522
-
keys, err := h.db.GetPublicKeys(did)
523
-
if err != nil {
524
-
h.WriteOOBNotice(w, "keys", "Failed to list keys. Try again later.")
525
-
log.Println(err)
526
-
return
527
-
}
528
-
529
-
data := make(map[string]interface{})
530
-
data["keys"] = keys
531
-
if err := h.t.ExecuteTemplate(w, "settings/keys", data); err != nil {
532
-
log.Println(err)
533
-
return
534
-
}
535
-
case http.MethodPut:
536
-
key := r.FormValue("key")
537
-
name := r.FormValue("name")
538
-
client, _ := h.auth.AuthorizedClient(r)
539
-
540
-
_, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))
541
-
if err != nil {
542
-
h.WriteOOBNotice(w, "keys", "Invalid public key. Check your formatting and try again.")
543
-
log.Printf("parsing public key: %s", err)
544
-
return
545
-
}
546
-
547
-
if err := h.db.AddPublicKey(did, name, key); err != nil {
548
-
h.WriteOOBNotice(w, "keys", "Failed to add key.")
549
-
log.Printf("adding public key: %s", err)
550
-
return
551
-
}
552
-
553
-
// store in pds too
554
-
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
555
-
Collection: "sh.bild.publicKey",
556
-
Repo: did,
557
-
Rkey: uuid.New().String(),
558
-
Record: &lexutil.LexiconTypeDecoder{Val: &shbild.PublicKey{
559
-
Created: time.Now().String(),
560
-
Key: key,
561
-
Name: name,
562
-
}},
563
-
})
564
-
565
-
// invalid record
566
-
if err != nil {
567
-
h.WriteOOBNotice(w, "keys", "Invalid inputs. Check your formatting and try again.")
568
-
log.Printf("failed to create record: %s", err)
569
-
return
570
-
}
571
-
572
-
log.Println("created atproto record: ", resp.Uri)
573
-
574
-
h.WriteOOBNotice(w, "keys", "Key added!")
575
-
return
576
-
}
577
-
}
578
-
579
-
func (h *Handle) NewRepo(w http.ResponseWriter, r *http.Request) {
580
-
session, _ := h.s.Get(r, "bild-session")
581
-
did := session.Values["did"].(string)
582
-
handle := session.Values["handle"].(string)
583
-
584
-
switch r.Method {
585
-
case http.MethodGet:
586
-
if err := h.t.ExecuteTemplate(w, "repo/new", nil); err != nil {
587
-
log.Println(err)
588
-
return
589
-
}
590
-
case http.MethodPut:
591
-
name := r.FormValue("name")
592
-
description := r.FormValue("description")
593
-
594
-
repoPath := filepath.Join(h.c.Repo.ScanPath, did, name)
595
-
err := git.InitBare(repoPath)
596
-
if err != nil {
597
-
h.WriteOOBNotice(w, "repo", "Error creating repo. Try again later.")
598
-
return
599
-
}
600
-
601
-
// For use by repoguard
602
-
didPath := filepath.Join(repoPath, "did")
603
-
err = os.WriteFile(didPath, []byte(did), 0644)
604
-
if err != nil {
605
-
h.WriteOOBNotice(w, "repo", "Error creating repo. Try again later.")
606
-
return
607
-
}
608
-
609
-
// TODO: add repo & setting-to-owner must happen in the same transaction
610
-
err = h.db.AddRepo(did, name, description)
611
-
if err != nil {
612
-
log.Println(err)
613
-
h.WriteOOBNotice(w, "repo", "Error creating repo. Try again later.")
614
-
return
615
-
}
616
-
// current user is set to owner of did/name repo
617
-
err = h.db.SetOwner(did, did, name)
618
-
if err != nil {
619
-
log.Println(err)
620
-
h.WriteOOBNotice(w, "repo", "Error creating repo. Try again later.")
621
-
return
622
-
}
623
-
624
-
w.Header().Set("HX-Redirect", fmt.Sprintf("/@%s/%s", handle, name))
625
-
w.WriteHeader(http.StatusOK)
626
-
}
627
-
}
628
-
629
-
func (h *Handle) Timeline(w http.ResponseWriter, r *http.Request) {
630
-
session, err := h.s.Get(r, "bild-session")
631
-
user := make(map[string]string)
632
-
if err != nil || session.IsNew {
633
-
// user is not logged in
634
-
} else {
635
-
user["handle"] = session.Values["handle"].(string)
636
-
user["did"] = session.Values["did"].(string)
637
-
}
638
-
639
-
if err := h.t.ExecuteTemplate(w, "timeline", user); err != nil {
640
-
log.Println(err)
641
-
return
642
-
}
643
-
}
-54
routes/tmpl/tmpl.go
-54
routes/tmpl/tmpl.go
···
1
-
package tmpl
2
-
3
-
import (
4
-
"html/template"
5
-
"log"
6
-
"os"
7
-
"path/filepath"
8
-
"strings"
9
-
)
10
-
11
-
func Load(tpath string) (*template.Template, error) {
12
-
tmpl := template.New("")
13
-
loadedTemplates := make(map[string]bool)
14
-
15
-
err := filepath.Walk(tpath, func(path string, info os.FileInfo, err error) error {
16
-
if err != nil {
17
-
return err
18
-
}
19
-
20
-
if !info.IsDir() && strings.HasSuffix(path, ".html") {
21
-
content, err := os.ReadFile(path)
22
-
if err != nil {
23
-
return err
24
-
}
25
-
26
-
relPath, err := filepath.Rel(tpath, path)
27
-
if err != nil {
28
-
return err
29
-
}
30
-
31
-
name := strings.TrimSuffix(relPath, ".html")
32
-
name = strings.ReplaceAll(name, string(filepath.Separator), "/")
33
-
34
-
_, err = tmpl.New(name).Parse(string(content))
35
-
if err != nil {
36
-
log.Printf("error parsing template %s: %v", name, err)
37
-
return err
38
-
}
39
-
40
-
loadedTemplates[name] = true
41
-
log.Printf("loaded template: %s", name)
42
-
return err
43
-
}
44
-
return nil
45
-
})
46
-
47
-
if err != nil {
48
-
return nil, err
49
-
}
50
-
51
-
log.Printf("total templates loaded: %d", len(loadedTemplates))
52
-
return tmpl, nil
53
-
54
-
}
-146
routes/util.go
-146
routes/util.go
···
1
-
package routes
2
-
3
-
import (
4
-
"fmt"
5
-
"io/fs"
6
-
"log"
7
-
"net/http"
8
-
"os"
9
-
"path/filepath"
10
-
"strings"
11
-
12
-
"github.com/go-chi/chi/v5"
13
-
"github.com/icyphox/bild/auth"
14
-
"github.com/icyphox/bild/git"
15
-
"github.com/microcosm-cc/bluemonday"
16
-
)
17
-
18
-
func sanitize(content []byte) []byte {
19
-
return bluemonday.UGCPolicy().SanitizeBytes([]byte(content))
20
-
}
21
-
22
-
func isGoModule(gr *git.GitRepo) bool {
23
-
_, err := gr.FileContent("go.mod")
24
-
return err == nil
25
-
}
26
-
27
-
func displayRepoName(r *http.Request) string {
28
-
user := r.Context().Value("did").(string)
29
-
name := chi.URLParam(r, "name")
30
-
31
-
handle, err := auth.ResolveIdent(r.Context(), user)
32
-
if err != nil {
33
-
log.Printf("failed to resolve ident: %s: %s", user, err)
34
-
return fmt.Sprintf("%s/%s", user, name)
35
-
}
36
-
37
-
return fmt.Sprintf("@%s/%s", handle.Handle.String(), name)
38
-
}
39
-
40
-
func didPath(r *http.Request) string {
41
-
did := r.Context().Value("did").(string)
42
-
path := filepath.Join(did, chi.URLParam(r, "name"))
43
-
filepath.Clean(path)
44
-
return path
45
-
}
46
-
47
-
func trimDotGit(name string) string {
48
-
return strings.TrimSuffix(name, ".git")
49
-
}
50
-
51
-
func getDescription(path string) (desc string) {
52
-
db, err := os.ReadFile(filepath.Join(path, "description"))
53
-
if err == nil {
54
-
desc = string(db)
55
-
} else {
56
-
desc = ""
57
-
}
58
-
return
59
-
}
60
-
61
-
func (h *Handle) isUnlisted(name string) bool {
62
-
for _, i := range h.c.Repo.Unlisted {
63
-
if name == i {
64
-
return true
65
-
}
66
-
}
67
-
68
-
return false
69
-
}
70
-
71
-
func (h *Handle) isIgnored(name string) bool {
72
-
for _, i := range h.c.Repo.Ignore {
73
-
if name == i {
74
-
return true
75
-
}
76
-
}
77
-
78
-
return false
79
-
}
80
-
81
-
type repoInfo struct {
82
-
Git *git.GitRepo
83
-
Path string
84
-
Category string
85
-
}
86
-
87
-
func (d *Handle) getAllRepos() ([]repoInfo, error) {
88
-
repos := []repoInfo{}
89
-
max := strings.Count(d.c.Repo.ScanPath, string(os.PathSeparator)) + 2
90
-
91
-
err := filepath.WalkDir(d.c.Repo.ScanPath, func(path string, de fs.DirEntry, err error) error {
92
-
if err != nil {
93
-
return err
94
-
}
95
-
96
-
if de.IsDir() {
97
-
// Check if we've exceeded our recursion depth
98
-
if strings.Count(path, string(os.PathSeparator)) > max {
99
-
return fs.SkipDir
100
-
}
101
-
102
-
if d.isIgnored(path) {
103
-
return fs.SkipDir
104
-
}
105
-
106
-
// A bare repo should always have at least a HEAD file, if it
107
-
// doesn't we can continue recursing
108
-
if _, err := os.Lstat(filepath.Join(path, "HEAD")); err == nil {
109
-
repo, err := git.Open(path, "")
110
-
if err != nil {
111
-
log.Println(err)
112
-
} else {
113
-
relpath, _ := filepath.Rel(d.c.Repo.ScanPath, path)
114
-
repos = append(repos, repoInfo{
115
-
Git: repo,
116
-
Path: relpath,
117
-
Category: d.category(path),
118
-
})
119
-
// Since we found a Git repo, we don't want to recurse
120
-
// further
121
-
return fs.SkipDir
122
-
}
123
-
}
124
-
}
125
-
return nil
126
-
})
127
-
128
-
return repos, err
129
-
}
130
-
131
-
func (d *Handle) category(path string) string {
132
-
return strings.TrimPrefix(filepath.Dir(strings.TrimPrefix(path, d.c.Repo.ScanPath)), string(os.PathSeparator))
133
-
}
134
-
135
-
func setContentDisposition(w http.ResponseWriter, name string) {
136
-
h := "inline; filename=\"" + name + "\""
137
-
w.Header().Add("Content-Disposition", h)
138
-
}
139
-
140
-
func setGZipMIME(w http.ResponseWriter) {
141
-
setMIME(w, "application/gzip")
142
-
}
143
-
144
-
func setMIME(w http.ResponseWriter, mime string) {
145
-
w.Header().Add("Content-Type", mime)
146
-
}
static/legit.png
static/legit.png
This is a binary file and will not be displayed.
-341
static/style.css
-341
static/style.css
···
1
-
:root {
2
-
--white: #fff;
3
-
--light: #f4f4f4;
4
-
--cyan: #509c93;
5
-
--light-gray: #eee;
6
-
--medium-gray: #ddd;
7
-
--gray: #6a6a6a;
8
-
--dark: #444;
9
-
--darker: #222;
10
-
11
-
--sans-font: -apple-system, BlinkMacSystemFont, "Inter", "Roboto",
12
-
"Segoe UI", sans-serif;
13
-
--display-font: -apple-system, BlinkMacSystemFont, "Inter", "Roboto",
14
-
"Segoe UI", sans-serif;
15
-
--mono-font: "SF Mono", SFMono-Regular, ui-monospace, "DejaVu Sans Mono",
16
-
"Roboto Mono", Menlo, Consolas, monospace;
17
-
}
18
-
19
-
@media (prefers-color-scheme: dark) {
20
-
:root {
21
-
color-scheme: dark light;
22
-
--white: #000;
23
-
--light: #181818;
24
-
--cyan: #76c7c0;
25
-
--light-gray: #333;
26
-
--medium-gray: #444;
27
-
--gray: #aaa;
28
-
--dark: #ddd;
29
-
--darker: #f4f4f4;
30
-
}
31
-
}
32
-
33
-
html {
34
-
background: var(--white);
35
-
-webkit-text-size-adjust: none;
36
-
font-family: var(--sans-font);
37
-
font-weight: 380;
38
-
}
39
-
40
-
pre {
41
-
font-family: var(--mono-font);
42
-
}
43
-
44
-
::selection {
45
-
background: var(--medium-gray);
46
-
opacity: 0.3;
47
-
}
48
-
49
-
* {
50
-
box-sizing: border-box;
51
-
padding: 0;
52
-
margin: 0;
53
-
}
54
-
55
-
body {
56
-
max-width: 1000px;
57
-
padding: 0 13px;
58
-
margin: 40px auto;
59
-
}
60
-
61
-
main,
62
-
footer {
63
-
font-size: 1rem;
64
-
padding: 0;
65
-
line-height: 160%;
66
-
}
67
-
68
-
header h1,
69
-
h2,
70
-
h3 {
71
-
font-family: var(--display-font);
72
-
}
73
-
74
-
h2 {
75
-
font-weight: 400;
76
-
}
77
-
78
-
strong {
79
-
font-weight: 500;
80
-
}
81
-
82
-
main h1 {
83
-
padding: 10px 0 10px 0;
84
-
}
85
-
86
-
main h2 {
87
-
font-size: 18px;
88
-
}
89
-
90
-
main h2,
91
-
h3 {
92
-
padding: 20px 0 15px 0;
93
-
}
94
-
95
-
nav {
96
-
padding: 0.4rem 0 1.5rem 0;
97
-
}
98
-
99
-
nav ul {
100
-
padding: 0;
101
-
margin: 0;
102
-
list-style: none;
103
-
padding-bottom: 20px;
104
-
}
105
-
106
-
nav ul li {
107
-
padding-right: 10px;
108
-
display: inline-block;
109
-
}
110
-
111
-
a {
112
-
margin: 0;
113
-
padding: 0;
114
-
box-sizing: border-box;
115
-
text-decoration: none;
116
-
word-wrap: break-word;
117
-
}
118
-
119
-
a {
120
-
color: var(--darker);
121
-
border-bottom: 1.5px solid var(--medium-gray);
122
-
}
123
-
124
-
a:hover {
125
-
border-bottom: 1.5px solid var(--gray);
126
-
}
127
-
128
-
.index {
129
-
padding-top: 2em;
130
-
display: grid;
131
-
grid-template-columns: 6em 1fr minmax(0, 7em);
132
-
grid-row-gap: 0.5em;
133
-
min-width: 0;
134
-
}
135
-
136
-
.clone-url {
137
-
padding-top: 2rem;
138
-
}
139
-
140
-
.clone-url pre {
141
-
color: var(--dark);
142
-
white-space: pre-wrap;
143
-
}
144
-
145
-
.desc {
146
-
font-weight: normal;
147
-
color: var(--gray);
148
-
font-style: italic;
149
-
}
150
-
151
-
.tree {
152
-
display: grid;
153
-
grid-template-columns: 10ch auto 1fr;
154
-
grid-row-gap: 0.5em;
155
-
grid-column-gap: 1em;
156
-
min-width: 0;
157
-
}
158
-
159
-
.log {
160
-
display: grid;
161
-
grid-template-columns: 20rem minmax(0, 1fr);
162
-
grid-row-gap: 0.8em;
163
-
grid-column-gap: 8rem;
164
-
margin-bottom: 2em;
165
-
padding-bottom: 1em;
166
-
border-bottom: 1.5px solid var(--medium-gray);
167
-
}
168
-
169
-
.log pre {
170
-
white-space: pre-wrap;
171
-
}
172
-
173
-
.mode,
174
-
.size {
175
-
font-family: var(--mono-font);
176
-
}
177
-
.size {
178
-
text-align: right;
179
-
}
180
-
181
-
.readme pre {
182
-
white-space: pre-wrap;
183
-
overflow-x: auto;
184
-
}
185
-
186
-
.readme {
187
-
background: var(--light-gray);
188
-
padding: 0.5rem;
189
-
}
190
-
191
-
.readme ul {
192
-
padding: revert;
193
-
}
194
-
195
-
.readme img {
196
-
max-width: 100%;
197
-
}
198
-
199
-
.diff {
200
-
margin: 1rem 0 1rem 0;
201
-
padding: 1rem 0 1rem 0;
202
-
border-bottom: 1.5px solid var(--medium-gray);
203
-
}
204
-
205
-
.diff pre {
206
-
overflow: scroll;
207
-
}
208
-
209
-
.diff-stat {
210
-
padding: 1rem 0 1rem 0;
211
-
}
212
-
213
-
.commit-hash,
214
-
.commit-email {
215
-
font-family: var(--mono-font);
216
-
}
217
-
218
-
.commit-email:before {
219
-
content: "<";
220
-
}
221
-
222
-
.commit-email:after {
223
-
content: ">";
224
-
}
225
-
226
-
.commit {
227
-
margin-bottom: 1rem;
228
-
}
229
-
230
-
.commit pre {
231
-
padding-bottom: 1rem;
232
-
white-space: pre-wrap;
233
-
}
234
-
235
-
.diff-stat ul li {
236
-
list-style: none;
237
-
padding-left: 0.5em;
238
-
}
239
-
240
-
.diff-add {
241
-
color: green;
242
-
}
243
-
244
-
.diff-del {
245
-
color: red;
246
-
}
247
-
248
-
.diff-noop {
249
-
color: var(--gray);
250
-
}
251
-
252
-
.ref {
253
-
font-family: var(--sans-font);
254
-
font-size: 14px;
255
-
color: var(--gray);
256
-
display: inline-block;
257
-
padding-top: 0.7em;
258
-
}
259
-
260
-
.refs pre {
261
-
white-space: pre-wrap;
262
-
padding-bottom: 0.5rem;
263
-
}
264
-
265
-
.refs strong {
266
-
padding-right: 1em;
267
-
}
268
-
269
-
.line-numbers {
270
-
white-space: pre-line;
271
-
-moz-user-select: -moz-none;
272
-
-khtml-user-select: none;
273
-
-webkit-user-select: none;
274
-
-o-user-select: none;
275
-
user-select: none;
276
-
display: flex;
277
-
float: left;
278
-
flex-direction: column;
279
-
margin-right: 1ch;
280
-
}
281
-
282
-
.file-wrapper {
283
-
display: flex;
284
-
flex-direction: row;
285
-
grid-template-columns: 1rem minmax(0, 1fr);
286
-
gap: 1rem;
287
-
padding: 0.5rem;
288
-
background: var(--light-gray);
289
-
overflow-x: auto;
290
-
}
291
-
292
-
.chroma-file-wrapper {
293
-
display: flex;
294
-
flex-direction: row;
295
-
grid-template-columns: 1rem minmax(0, 1fr);
296
-
overflow-x: auto;
297
-
}
298
-
299
-
.file-content {
300
-
background: var(--light-gray);
301
-
overflow-y: hidden;
302
-
overflow-x: auto;
303
-
}
304
-
305
-
.diff-type {
306
-
color: var(--gray);
307
-
}
308
-
309
-
.commit-info {
310
-
color: var(--gray);
311
-
padding-bottom: 1.5rem;
312
-
font-size: 0.85rem;
313
-
}
314
-
315
-
@media (max-width: 600px) {
316
-
.index {
317
-
grid-row-gap: 0.8em;
318
-
}
319
-
320
-
.log {
321
-
grid-template-columns: 1fr;
322
-
grid-row-gap: 0em;
323
-
}
324
-
325
-
.index {
326
-
grid-template-columns: 1fr;
327
-
grid-row-gap: 0em;
328
-
}
329
-
330
-
.index-name:not(:first-child) {
331
-
padding-top: 1.5rem;
332
-
}
333
-
334
-
.commit-info:not(:last-child) {
335
-
padding-bottom: 1.5rem;
336
-
}
337
-
338
-
pre {
339
-
font-size: 0.8rem;
340
-
}
341
-
}
-10
templates/errors/404.html
-10
templates/errors/404.html
-10
templates/errors/500.html
-10
templates/errors/500.html
-21
templates/index.html
-21
templates/index.html
···
1
-
{{ define "index" }}
2
-
<html>
3
-
{{ template "layouts/head" . }}
4
-
5
-
<header>
6
-
<h1>{{ .meta.Title }}</h1>
7
-
<h2>{{ .meta.Description }}</h2>
8
-
</header>
9
-
<body>
10
-
<main>
11
-
<div class="index">
12
-
{{ range .info }}
13
-
<div class="index-name"><a href="/{{ .Name }}">{{ .DisplayName }}</a></div>
14
-
<div class="desc">{{ .Desc }}</div>
15
-
<div>{{ .Idle }}</div>
16
-
{{ end }}
17
-
</div>
18
-
</main>
19
-
</body>
20
-
</html>
21
-
{{ end }}
-37
templates/layouts/head.html
-37
templates/layouts/head.html
···
1
-
<head>
2
-
<meta charset="utf-8" />
3
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
4
-
<link rel="stylesheet" href="/static/style.css" type="text/css" />
5
-
<link rel="icon" type="image/png" size="32x32" href="/static/legit.png" />
6
-
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
7
-
<meta name="htmx-config" content='{"selfRequestsOnly":false}' />
8
-
9
-
{{ if .parent }}
10
-
<title>
11
-
{{ .meta.Title }} — {{ .name }} ({{ .ref }}): {{ .parent }}/
12
-
</title>
13
-
14
-
{{ else if .path }}
15
-
<title>
16
-
{{ .meta.Title }} — {{ .name }} ({{ .ref }}): {{ .path }}
17
-
</title>
18
-
{{ else if .files }}
19
-
<title>{{ .meta.Title }} — {{ .name }} ({{ .ref }})</title>
20
-
{{ else if .commit }}
21
-
<title>{{ .meta.Title }} — {{ .name }}: {{ .commit.This }}</title>
22
-
{{ else if .branches }}
23
-
<title>{{ .meta.Title }} — {{ .name }}: refs</title>
24
-
{{ else if .commits }} {{ if .log }}
25
-
<title>{{ .meta.Title }} — {{ .name }}: log</title>
26
-
{{ else }}
27
-
<title>{{ .meta.Title }} — {{ .name }}</title>
28
-
{{ end }} {{ else }}
29
-
<title>{{ .meta.Title }}</title>
30
-
{{ end }} {{ if and .servername .gomod }}
31
-
<meta
32
-
name="go-import"
33
-
content="{{ .servername}}/{{ .name }} git https://{{ .servername }}/{{ .name }}"
34
-
/>
35
-
{{ end }}
36
-
<!-- other meta tags here -->
37
-
</head>
-9
templates/layouts/repo-header.html
-9
templates/layouts/repo-header.html
-12
templates/layouts/topbar.html
-12
templates/layouts/topbar.html
-100
templates/repo/commit.html
-100
templates/repo/commit.html
···
1
-
<html>
2
-
{{ template "layouts/head" . }}
3
-
4
-
{{ template "layouts/repo-header" . }}
5
-
<body>
6
-
{{ template "layouts/nav" . }}
7
-
<main>
8
-
<section class="commit">
9
-
<pre>{{- .commit.Message -}}</pre>
10
-
<div class="commit-info">
11
-
{{ .commit.Author.Name }} <a href="mailto:{{ .commit.Author.Email }}" class="commit-email">{{ .commit.Author.Email}}</a>
12
-
<div>{{ .commit.Author.When.Format "Mon, 02 Jan 2006 15:04:05 -0700" }}</div>
13
-
</div>
14
-
15
-
<div>
16
-
<strong>commit</strong>
17
-
<p><a href="/{{ .name }}/commit/{{ .commit.This }}" class="commit-hash">
18
-
{{ .commit.This }}
19
-
</a>
20
-
</p>
21
-
</div>
22
-
23
-
{{ if .commit.Parent }}
24
-
<div>
25
-
<strong>parent</strong>
26
-
<p><a href="/{{ .name }}/commit/{{ .commit.Parent }}" class="commit-hash">
27
-
{{ .commit.Parent }}
28
-
</a></p>
29
-
</div>
30
-
31
-
{{ end }}
32
-
<div class="diff-stat">
33
-
<div>
34
-
{{ .stat.FilesChanged }} files changed,
35
-
{{ .stat.Insertions }} insertions(+),
36
-
{{ .stat.Deletions }} deletions(-)
37
-
</div>
38
-
<div>
39
-
<br>
40
-
<strong>jump to</strong>
41
-
{{ range .diff }}
42
-
<ul>
43
-
<li><a href="#{{ .Name.New }}">{{ .Name.New }}</a></li>
44
-
</ul>
45
-
{{ end }}
46
-
</div>
47
-
</div>
48
-
</section>
49
-
<section>
50
-
{{ $repo := .name }}
51
-
{{ $this := .commit.This }}
52
-
{{ $parent := .commit.Parent }}
53
-
{{ range .diff }}
54
-
<div id="{{ .Name.New }}">
55
-
<div class="diff">
56
-
{{ if .IsNew }}
57
-
<span class="diff-type">A</span>
58
-
{{ end }}
59
-
{{ if .IsDelete }}
60
-
<span class="diff-type">D</span>
61
-
{{ end }}
62
-
{{ if not (or .IsNew .IsDelete) }}
63
-
<span class="diff-type">M</span>
64
-
{{ end }}
65
-
{{ if .Name.Old }}
66
-
<a href="/{{ $repo }}/blob/{{ $parent }}/{{ .Name.Old }}">{{ .Name.Old }}</a>
67
-
{{ if .Name.New }}
68
-
→
69
-
<a href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.New }}">{{ .Name.New }}</a>
70
-
{{ end }}
71
-
{{ else }}
72
-
<a href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.New }}">{{ .Name.New }}</a>
73
-
{{- end -}}
74
-
{{ if .IsBinary }}
75
-
<p>Not showing binary file.</p>
76
-
{{ else }}
77
-
<pre>
78
-
{{- range .TextFragments -}}
79
-
<p>{{- .Header -}}</p>
80
-
{{- range .Lines -}}
81
-
{{- if eq .Op.String "+" -}}
82
-
<span class="diff-add">{{ .String }}</span>
83
-
{{- end -}}
84
-
{{- if eq .Op.String "-" -}}
85
-
<span class="diff-del">{{ .String }}</span>
86
-
{{- end -}}
87
-
{{- if eq .Op.String " " -}}
88
-
<span class="diff-noop">{{ .String }}</span>
89
-
{{- end -}}
90
-
{{- end -}}
91
-
{{- end -}}
92
-
{{- end -}}
93
-
</pre>
94
-
</div>
95
-
</div>
96
-
{{ end }}
97
-
</section>
98
-
</main>
99
-
</body>
100
-
</html>
-9
templates/repo/empty.html
-9
templates/repo/empty.html
-34
templates/repo/file.html
-34
templates/repo/file.html
···
1
-
<html>
2
-
{{ template "layouts/head" . }}
3
-
{{ template "layouts/repo-header" . }}
4
-
<body>
5
-
{{ template "layouts/nav" . }}
6
-
<main>
7
-
<p>{{ .path }} (<a style="color: gray" href="?raw=true">view raw</a>)</p>
8
-
{{if .chroma }}
9
-
<div class="chroma-file-wrapper">
10
-
{{ .content }}
11
-
</div>
12
-
{{else}}
13
-
<div class="file-wrapper">
14
-
<table>
15
-
<tbody><tr>
16
-
<td class="line-numbers">
17
-
<pre>
18
-
{{- range .linecount }}
19
-
<a id="L{{ . }}" href="#L{{ . }}">{{ . }}</a>
20
-
{{- end -}}
21
-
</pre>
22
-
</td>
23
-
<td class="file-content">
24
-
<pre>
25
-
{{- .content -}}
26
-
</pre>
27
-
</td>
28
-
</tbody></tr>
29
-
</table>
30
-
</div>
31
-
{{end}}
32
-
</main>
33
-
</body>
34
-
</html>
-23
templates/repo/log.html
-23
templates/repo/log.html
···
1
-
<html>
2
-
{{ template "layouts/head" . }}
3
-
4
-
{{ template "layouts/repo-header" . }}
5
-
<body>
6
-
{{ template "layouts/nav" . }}
7
-
<main>
8
-
{{ $repo := .name }}
9
-
<div class="log">
10
-
{{ range .commits }}
11
-
<div>
12
-
<div><a href="/{{ $repo }}/commit/{{ .Hash.String }}" class="commit-hash">{{ slice .Hash.String 0 8 }}</a></div>
13
-
<pre>{{ .Message }}</pre>
14
-
</div>
15
-
<div class="commit-info">
16
-
{{ .Author.Name }} <a href="mailto:{{ .Author.Email }}" class="commit-email">{{ .Author.Email }}</a>
17
-
<div>{{ .Author.When.Format "Mon, 02 Jan 2006 15:04:05 -0700" }}</div>
18
-
</div>
19
-
{{ end }}
20
-
</div>
21
-
</main>
22
-
</body>
23
-
</html>
-30
templates/repo/new.html
-30
templates/repo/new.html
···
1
-
<html>
2
-
{{ template "layouts/head" . }}
3
-
4
-
<body>
5
-
<main>
6
-
<h1>create a new repository</h1>
7
-
<form>
8
-
<p>
9
-
Give your Git repository a name and, optionally, a
10
-
description.
11
-
</p>
12
-
<div id="repo"></div>
13
-
<div>
14
-
<label for="name">Name</label>
15
-
<input type="text" id="name" name="name" placeholder="" />
16
-
<label for="description">Description</label>
17
-
<input
18
-
type="text"
19
-
id="description"
20
-
name="description"
21
-
placeholder=""
22
-
/>
23
-
</div>
24
-
<button hx-put="/repo/new" hx-swap="none" type="submit">
25
-
Submit
26
-
</button>
27
-
</form>
28
-
</main>
29
-
</body>
30
-
</html>
-38
templates/repo/refs.html
-38
templates/repo/refs.html
···
1
-
<html>
2
-
{{ template "layouts/head" . }}
3
-
4
-
{{ template "layouts/repo-header" . }}
5
-
<body>
6
-
{{ template "layouts/nav" . }}
7
-
<main>
8
-
{{ $name := .name }}
9
-
<h3>branches</h3>
10
-
<div class="refs">
11
-
{{ range .branches }}
12
-
<div>
13
-
<strong>{{ .Name.Short }}</strong>
14
-
<a href="/{{ $name }}/tree/{{ .Name.Short }}/">browse</a>
15
-
<a href="/{{ $name }}/log/{{ .Name.Short }}">log</a>
16
-
<a href="/{{ $name }}/archive/{{ .Name.Short }}.tar.gz">tar.gz</a>
17
-
</div>
18
-
{{ end }}
19
-
</div>
20
-
{{ if .tags }}
21
-
<h3>tags</h3>
22
-
<div class="refs">
23
-
{{ range .tags }}
24
-
<div>
25
-
<strong>{{ .Name }}</strong>
26
-
<a href="/{{ $name }}/tree/{{ .Name }}/">browse</a>
27
-
<a href="/{{ $name }}/log/{{ .Name }}">log</a>
28
-
<a href="/{{ $name }}/archive/{{ .Name }}.tar.gz">tar.gz</a>
29
-
{{ if .Message }}
30
-
<pre>{{ .Message }}</pre>
31
-
</div>
32
-
{{ end }}
33
-
{{ end }}
34
-
</div>
35
-
{{ end }}
36
-
</main>
37
-
</body>
38
-
</html>
-36
templates/repo/repo.html
-36
templates/repo/repo.html
···
1
-
<html>
2
-
{{ template "layouts/head" . }}
3
-
4
-
{{ template "layouts/repo-header" . }}
5
-
6
-
<body>
7
-
{{ template "layouts/nav" . }}
8
-
<main>
9
-
{{ $repo := .name }}
10
-
<div class="log">
11
-
{{ range .commits }}
12
-
<div>
13
-
<div><a href="/{{ $repo }}/commit/{{ .Hash.String }}" class="commit-hash">{{ slice .Hash.String 0 8 }}</a></div>
14
-
<pre>{{ .Message }}</pre>
15
-
</div>
16
-
<div class="commit-info">
17
-
{{ .Author.Name }} <a href="mailto:{{ .Author.Email }}" class="commit-email">{{ .Author.Email }}</a>
18
-
<div>{{ .Author.When.Format "Mon, 02 Jan 2006 15:04:05 -0700" }}</div>
19
-
</div>
20
-
{{ end }}
21
-
</div>
22
-
{{- if .readme }}
23
-
<article class="readme">
24
-
{{- .readme -}}
25
-
</article>
26
-
{{- end -}}
27
-
28
-
<div class="clone-url">
29
-
<strong>clone</strong>
30
-
<pre>
31
-
git clone https://{{ .servername }}/{{ .name }}
32
-
</pre>
33
-
</div>
34
-
</main>
35
-
</body>
36
-
</html>
-53
templates/repo/tree.html
-53
templates/repo/tree.html
···
1
-
<html>
2
-
3
-
{{ template "layouts/head" . }}
4
-
5
-
{{ template "layouts/repo-header" . }}
6
-
<body>
7
-
{{ template "layouts/nav" . }}
8
-
<main>
9
-
{{ $repo := .name }}
10
-
{{ $ref := .ref }}
11
-
{{ $parent := .parent }}
12
-
13
-
<div class="tree">
14
-
{{ if $parent }}
15
-
<div></div>
16
-
<div></div>
17
-
<div><a href="/{{ $repo }}/tree/{{ $ref }}/{{ .dotdot }}">..</a></div>
18
-
{{ end }}
19
-
{{ range .files }}
20
-
{{ if not .IsFile }}
21
-
<div class="mode">{{ .Mode }}</div>
22
-
<div class="size">{{ .Size }}</div>
23
-
<div>
24
-
{{ if $parent }}
25
-
<a href="/{{ $repo }}/tree/{{ $ref }}/{{ $parent }}/{{ .Name }}">{{ .Name }}/</a>
26
-
{{ else }}
27
-
<a href="/{{ $repo }}/tree/{{ $ref }}/{{ .Name }}">{{ .Name }}/</a>
28
-
{{ end }}
29
-
</div>
30
-
{{ end }}
31
-
{{ end }}
32
-
{{ range .files }}
33
-
{{ if .IsFile }}
34
-
<div class="mode">{{ .Mode }}</div>
35
-
<div class="size">{{ .Size }}</div>
36
-
<div>
37
-
{{ if $parent }}
38
-
<a href="/{{ $repo }}/blob/{{ $ref }}/{{ $parent }}/{{ .Name }}">{{ .Name }}</a>
39
-
{{ else }}
40
-
<a href="/{{ $repo }}/blob/{{ $ref }}/{{ .Name }}">{{ .Name }}</a>
41
-
{{ end }}
42
-
</div>
43
-
{{ end }}
44
-
{{ end }}
45
-
</div>
46
-
<article>
47
-
<pre>
48
-
{{- if .readme }}{{ .readme }}{{- end -}}
49
-
</pre>
50
-
</article>
51
-
</main>
52
-
</body>
53
-
</html>
-51
templates/settings/keys.html
-51
templates/settings/keys.html
···
1
-
<html>
2
-
{{ template "layouts/head" . }}
3
-
4
-
<body>
5
-
<main>
6
-
<form>
7
-
<p>
8
-
Give your key a name and paste your
9
-
<strong>public</strong> key here. This is what you'll use to
10
-
push to your Git repository.
11
-
</p>
12
-
<div id="keys"></div>
13
-
<div>
14
-
<input
15
-
type="text"
16
-
id="name"
17
-
name="name"
18
-
placeholder="my laptop"
19
-
/>
20
-
<input
21
-
type="text"
22
-
id="public_key"
23
-
name="key"
24
-
placeholder="ssh-ed25519 AAABBBHUNTER2..."
25
-
/>
26
-
</div>
27
-
<button hx-put="/settings/keys" hx-swap="none" type="submit">
28
-
Submit
29
-
</button>
30
-
</form>
31
-
<table>
32
-
<thead>
33
-
<tr>
34
-
<th>Key</th>
35
-
<th>Name</th>
36
-
<th>Created</th>
37
-
</tr>
38
-
</thead>
39
-
<tbody>
40
-
{{ range .keys }}
41
-
<tr>
42
-
<td>{{ .Name }}</td>
43
-
<td>{{ .Created }}</td>
44
-
<td>{{ .Key }}</td>
45
-
</tr>
46
-
{{ end }}
47
-
</tbody>
48
-
</table>
49
-
</main>
50
-
</body>
51
-
</html>
-14
templates/timeline.html
-14
templates/timeline.html
-29
templates/user/login.html
-29
templates/user/login.html
···
1
-
<html>
2
-
{{ template "layouts/head" . }}
3
-
4
-
<body>
5
-
<main>
6
-
<form class="form-login" method="post" action="/login">
7
-
<p>
8
-
You will be redirected to bsky.social (or your PDS) to
9
-
complete login.
10
-
</p>
11
-
<div>
12
-
<input
13
-
type="text"
14
-
id="username"
15
-
name="username"
16
-
placeholder="@username.bsky.social"
17
-
/>
18
-
<input
19
-
type="password"
20
-
id="app_password"
21
-
name="app_password"
22
-
placeholder="app password"
23
-
/>
24
-
</div>
25
-
<button type="submit">Login</button>
26
-
</form>
27
-
</main>
28
-
</body>
29
-
</html>