this repo has no description

delete outdated legit code

Akshay 166abe60 8ff2760c

-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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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

This is a binary file and will not be displayed.

-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
··· 1 - <html> 2 - <title>404</title> 3 - {{ template "layouts/head" . }} 4 - <body> 5 - {{ template "layouts/nav" . }} 6 - <main> 7 - <h3>404 &mdash; nothing like that here.</h3> 8 - </main> 9 - </body> 10 - </html>
-10
templates/errors/500.html
··· 1 - <html> 2 - <title>500</title> 3 - {{ template "layouts/head" . }} 4 - <body> 5 - {{ template "layouts/nav" . }} 6 - <main> 7 - <h3>500 &mdash; something broke!</h3> 8 - </main> 9 - </body> 10 - </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
··· 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 }} &mdash; {{ .name }} ({{ .ref }}): {{ .parent }}/ 12 - </title> 13 - 14 - {{ else if .path }} 15 - <title> 16 - {{ .meta.Title }} &mdash; {{ .name }} ({{ .ref }}): {{ .path }} 17 - </title> 18 - {{ else if .files }} 19 - <title>{{ .meta.Title }} &mdash; {{ .name }} ({{ .ref }})</title> 20 - {{ else if .commit }} 21 - <title>{{ .meta.Title }} &mdash; {{ .name }}: {{ .commit.This }}</title> 22 - {{ else if .branches }} 23 - <title>{{ .meta.Title }} &mdash; {{ .name }}: refs</title> 24 - {{ else if .commits }} {{ if .log }} 25 - <title>{{ .meta.Title }} &mdash; {{ .name }}: log</title> 26 - {{ else }} 27 - <title>{{ .meta.Title }} &mdash; {{ .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>
-12
templates/layouts/nav.html
··· 1 - <nav> 2 - <ul> 3 - {{ if .name }} 4 - <li><a href="/{{ .name }}">summary</a></li> 5 - <li><a href="/{{ .name }}/refs">refs</a> {{ if .ref }}</li> 6 - 7 - <li><a href="/{{ .name }}/tree/{{ .ref }}/">tree</a></li> 8 - <li> 9 - <a href="/{{ .name }}/log/{{ .ref }}">log</a> {{ end }} {{ end }} 10 - </li> 11 - </ul> 12 - </nav>
-9
templates/layouts/repo-header.html
··· 1 - <header> 2 - <h2> 3 - <a href="/">all repos</a> 4 - &mdash; {{ .displayname }} {{ if .ref }} 5 - <span class="ref">@ {{ .ref }}</span> 6 - {{ end }} 7 - </h2> 8 - <h3 class="desc">{{ .desc }}</h3> 9 - </header>
-12
templates/layouts/topbar.html
··· 1 - <nav> 2 - <ul> 3 - {{ if . }} 4 - <li>logged in as 5 - <a href="/@{{ .handle }}">{{ .handle }}</a> (with {{ .did }}) 6 - </li> 7 - {{ else }} 8 - <li><a href="/login">login</a></li> 9 - {{ end }} 10 - </ul> 11 - </nav> 12 -
-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 - &#8594; 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
··· 1 - <html> 2 - {{ template "layouts/head" . }} 3 - 4 - <body> 5 - <main> 6 - <p>This is an empty Git repository. Push some commits here.</p> 7 - </main> 8 - </body> 9 - </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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 1 - {{ define "timeline" }} 2 - <html> 3 - {{ template "layouts/head" . }} 4 - 5 - <header> 6 - <h1>timeline</h1> 7 - </header> 8 - <body> 9 - <main> 10 - {{ template "layouts/topbar" . }} 11 - </main> 12 - </body> 13 - </html> 14 - {{ end }}
-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>