+41
cmd/knotserver/main.go
+41
cmd/knotserver/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
+
addr := fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port)
38
+
39
+
log.Println("starting main server on", addr)
40
+
go http.ListenAndServe(addr, mux)
41
+
}
+1
go.mod
+1
go.mod
···
9
9
github.com/bluekeyes/go-gitdiff v0.8.0
10
10
github.com/bluesky-social/indigo v0.0.0-20250123072624-9e3b84fdbb20
11
11
github.com/dustin/go-humanize v1.0.1
12
+
github.com/go-chi/chi v1.5.5
12
13
github.com/go-chi/chi/v5 v5.2.0
13
14
github.com/go-git/go-git/v5 v5.12.0
14
15
github.com/google/uuid v1.6.0
+2
go.sum
+2
go.sum
···
53
53
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
54
54
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
55
55
github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
56
+
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
57
+
github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw=
56
58
github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0=
57
59
github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
58
60
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
+74
knotserver/file.go
+74
knotserver/file.go
···
1
+
package knotserver
2
+
3
+
import (
4
+
"bytes"
5
+
"io"
6
+
"log"
7
+
"net/http"
8
+
"strings"
9
+
10
+
"github.com/icyphox/bild/git"
11
+
)
12
+
13
+
func (h *Handle) listFiles(files []git.NiceTree, data map[string]any, w http.ResponseWriter) {
14
+
data["files"] = files
15
+
data["meta"] = h.c.Meta
16
+
17
+
writeJSON(w, data)
18
+
return
19
+
}
20
+
21
+
func countLines(r io.Reader) (int, error) {
22
+
buf := make([]byte, 32*1024)
23
+
bufLen := 0
24
+
count := 0
25
+
nl := []byte{'\n'}
26
+
27
+
for {
28
+
c, err := r.Read(buf)
29
+
if c > 0 {
30
+
bufLen += c
31
+
}
32
+
count += bytes.Count(buf[:c], nl)
33
+
34
+
switch {
35
+
case err == io.EOF:
36
+
/* handle last line not having a newline at the end */
37
+
if bufLen >= 1 && buf[(bufLen-1)%(32*1024)] != '\n' {
38
+
count++
39
+
}
40
+
return count, nil
41
+
case err != nil:
42
+
return 0, err
43
+
}
44
+
}
45
+
}
46
+
47
+
func (h *Handle) showFile(content string, data map[string]any, w http.ResponseWriter) {
48
+
lc, err := countLines(strings.NewReader(content))
49
+
if err != nil {
50
+
// Non-fatal, we'll just skip showing line numbers in the template.
51
+
log.Printf("counting lines: %s", err)
52
+
}
53
+
54
+
lines := make([]int, lc)
55
+
if lc > 0 {
56
+
for i := range lines {
57
+
lines[i] = i + 1
58
+
}
59
+
}
60
+
61
+
data["linecount"] = lines
62
+
data["content"] = content
63
+
data["meta"] = h.c.Meta
64
+
65
+
writeJSON(w, data)
66
+
return
67
+
}
68
+
69
+
func (h *Handle) showRaw(content string, w http.ResponseWriter) {
70
+
w.WriteHeader(http.StatusOK)
71
+
w.Header().Set("Content-Type", "text/plain")
72
+
w.Write([]byte(content))
73
+
return
74
+
}
+68
knotserver/git.go
+68
knotserver/git.go
···
1
+
package knotserver
2
+
3
+
import (
4
+
"compress/gzip"
5
+
"io"
6
+
"log"
7
+
"net/http"
8
+
"path/filepath"
9
+
10
+
"github.com/icyphox/bild/knotserver/git/service"
11
+
)
12
+
func (d *Handle) InfoRefs(w http.ResponseWriter, r *http.Request) {
13
+
name := displayRepoName(r)
14
+
name = filepath.Clean(name)
15
+
16
+
repo := filepath.Join(d.c.Repo.ScanPath, name)
17
+
18
+
w.Header().Set("content-type", "application/x-git-upload-pack-advertisement")
19
+
w.WriteHeader(http.StatusOK)
20
+
21
+
cmd := service.ServiceCommand{
22
+
Dir: repo,
23
+
Stdout: w,
24
+
}
25
+
26
+
if err := cmd.InfoRefs(); err != nil {
27
+
http.Error(w, err.Error(), 500)
28
+
log.Printf("git: failed to execute git-upload-pack (info/refs) %s", err)
29
+
return
30
+
}
31
+
}
32
+
33
+
func (d *Handle) UploadPack(w http.ResponseWriter, r *http.Request) {
34
+
name := displayRepoName(r)
35
+
name = filepath.Clean(name)
36
+
37
+
repo := filepath.Join(d.c.Repo.ScanPath, name)
38
+
39
+
w.Header().Set("content-type", "application/x-git-upload-pack-result")
40
+
w.Header().Set("Connection", "Keep-Alive")
41
+
w.Header().Set("Transfer-Encoding", "chunked")
42
+
w.WriteHeader(http.StatusOK)
43
+
44
+
cmd := service.ServiceCommand{
45
+
Dir: repo,
46
+
Stdout: w,
47
+
}
48
+
49
+
var reader io.ReadCloser
50
+
reader = r.Body
51
+
52
+
if r.Header.Get("Content-Encoding") == "gzip" {
53
+
reader, err := gzip.NewReader(r.Body)
54
+
if err != nil {
55
+
http.Error(w, err.Error(), 500)
56
+
log.Printf("git: failed to create gzip reader: %s", err)
57
+
return
58
+
}
59
+
defer reader.Close()
60
+
}
61
+
62
+
cmd.Stdin = reader
63
+
if err := cmd.UploadPack(); err != nil {
64
+
http.Error(w, err.Error(), 500)
65
+
log.Printf("git: failed to execute git-upload-pack %s", err)
66
+
return
67
+
}
68
+
}
+119
knotserver/git/diff.go
+119
knotserver/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
+
}
+344
knotserver/git/git.go
+344
knotserver/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
+
for _, b := range branches {
206
+
_, err := g.r.ResolveRevision(plumbing.Revision(b))
207
+
if err == nil {
208
+
return b, nil
209
+
}
210
+
}
211
+
return "", fmt.Errorf("unable to find main branch")
212
+
}
213
+
214
+
// WriteTar writes itself from a tree into a binary tar file format.
215
+
// prefix is root folder to be appended.
216
+
func (g *GitRepo) WriteTar(w io.Writer, prefix string) error {
217
+
tw := tar.NewWriter(w)
218
+
defer tw.Close()
219
+
220
+
c, err := g.r.CommitObject(g.h)
221
+
if err != nil {
222
+
return fmt.Errorf("commit object: %w", err)
223
+
}
224
+
225
+
tree, err := c.Tree()
226
+
if err != nil {
227
+
return err
228
+
}
229
+
230
+
walker := object.NewTreeWalker(tree, true, nil)
231
+
defer walker.Close()
232
+
233
+
name, entry, err := walker.Next()
234
+
for ; err == nil; name, entry, err = walker.Next() {
235
+
info, err := newInfoWrapper(name, prefix, &entry, tree)
236
+
if err != nil {
237
+
return err
238
+
}
239
+
240
+
header, err := tar.FileInfoHeader(info, "")
241
+
if err != nil {
242
+
return err
243
+
}
244
+
245
+
err = tw.WriteHeader(header)
246
+
if err != nil {
247
+
return err
248
+
}
249
+
250
+
if !info.IsDir() {
251
+
file, err := tree.File(name)
252
+
if err != nil {
253
+
return err
254
+
}
255
+
256
+
reader, err := file.Blob.Reader()
257
+
if err != nil {
258
+
return err
259
+
}
260
+
261
+
_, err = io.Copy(tw, reader)
262
+
if err != nil {
263
+
reader.Close()
264
+
return err
265
+
}
266
+
reader.Close()
267
+
}
268
+
}
269
+
270
+
return nil
271
+
}
272
+
273
+
func newInfoWrapper(
274
+
name string,
275
+
prefix string,
276
+
entry *object.TreeEntry,
277
+
tree *object.Tree,
278
+
) (*infoWrapper, error) {
279
+
var (
280
+
size int64
281
+
mode fs.FileMode
282
+
isDir bool
283
+
)
284
+
285
+
if entry.Mode.IsFile() {
286
+
file, err := tree.TreeEntryFile(entry)
287
+
if err != nil {
288
+
return nil, err
289
+
}
290
+
mode = fs.FileMode(file.Mode)
291
+
292
+
size, err = tree.Size(name)
293
+
if err != nil {
294
+
return nil, err
295
+
}
296
+
} else {
297
+
isDir = true
298
+
mode = fs.ModeDir | fs.ModePerm
299
+
}
300
+
301
+
fullname := path.Join(prefix, name)
302
+
return &infoWrapper{
303
+
name: fullname,
304
+
size: size,
305
+
mode: mode,
306
+
modTime: time.Unix(0, 0),
307
+
isDir: isDir,
308
+
}, nil
309
+
}
310
+
311
+
func (i *infoWrapper) Name() string {
312
+
return i.name
313
+
}
314
+
315
+
func (i *infoWrapper) Size() int64 {
316
+
return i.size
317
+
}
318
+
319
+
func (i *infoWrapper) Mode() fs.FileMode {
320
+
return i.mode
321
+
}
322
+
323
+
func (i *infoWrapper) ModTime() time.Time {
324
+
return i.modTime
325
+
}
326
+
327
+
func (i *infoWrapper) IsDir() bool {
328
+
return i.isDir
329
+
}
330
+
331
+
func (i *infoWrapper) Sys() any {
332
+
return nil
333
+
}
334
+
335
+
func (t *TagReference) Name() string {
336
+
return t.ref.Name().Short()
337
+
}
338
+
339
+
func (t *TagReference) Message() string {
340
+
if t.tag != nil {
341
+
return t.tag.Message
342
+
}
343
+
return ""
344
+
}
+33
knotserver/git/repo.go
+33
knotserver/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
knotserver/git/service/service.go
+121
knotserver/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
knotserver/git/service/write_flusher.go
+25
knotserver/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
knotserver/git/tree.go
+66
knotserver/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
+
}
+78
knotserver/handler.go
+78
knotserver/handler.go
···
1
+
package knotserver
2
+
3
+
import (
4
+
"net/http"
5
+
6
+
"github.com/go-chi/chi"
7
+
"github.com/icyphox/bild/config"
8
+
"github.com/icyphox/bild/db"
9
+
)
10
+
11
+
func Setup(c *config.Config, db *db.DB) (http.Handler, error) {
12
+
r := chi.NewRouter()
13
+
14
+
h := Handle{
15
+
c: c,
16
+
db: db,
17
+
}
18
+
19
+
// r.Route("/repo", func(r chi.Router) {
20
+
// r.Use(h.AuthMiddleware)
21
+
// r.Get("/new", h.NewRepo)
22
+
// r.Put("/new", h.NewRepo)
23
+
// })
24
+
25
+
r.Route("/{did}", func(r chi.Router) {
26
+
r.Get("/", h.Index)
27
+
28
+
// Repo routes
29
+
r.Route("/{name}", func(r chi.Router) {
30
+
r.Get("/", h.Multiplex)
31
+
r.Post("/", h.Multiplex)
32
+
33
+
r.Route("/tree/{ref}", func(r chi.Router) {
34
+
r.Get("/*", h.RepoTree)
35
+
})
36
+
37
+
r.Route("/blob/{ref}", func(r chi.Router) {
38
+
r.Get("/*", h.FileContent)
39
+
})
40
+
41
+
r.Get("/log/{ref}", h.Log)
42
+
r.Get("/archive/{file}", h.Archive)
43
+
r.Get("/commit/{ref}", h.Diff)
44
+
r.Get("/refs/", h.Refs)
45
+
46
+
// Catch-all routes
47
+
r.Get("/*", h.Multiplex)
48
+
r.Post("/*", h.Multiplex)
49
+
})
50
+
})
51
+
52
+
return r, nil
53
+
}
54
+
55
+
type Handle struct {
56
+
c *config.Config
57
+
db *db.DB
58
+
}
59
+
60
+
func (h *Handle) Multiplex(w http.ResponseWriter, r *http.Request) {
61
+
path := chi.URLParam(r, "*")
62
+
63
+
if r.URL.RawQuery == "service=git-receive-pack" {
64
+
w.WriteHeader(http.StatusBadRequest)
65
+
w.Write([]byte("no pushing allowed!"))
66
+
return
67
+
}
68
+
69
+
if path == "info/refs" &&
70
+
r.URL.RawQuery == "service=git-upload-pack" &&
71
+
r.Method == "GET" {
72
+
h.InfoRefs(w, r)
73
+
} else if path == "git-upload-pack" && r.Method == "POST" {
74
+
h.UploadPack(w, r)
75
+
} else if r.Method == "GET" {
76
+
h.RepoIndex(w, r)
77
+
}
78
+
}
+26
knotserver/http_util.go
+26
knotserver/http_util.go
···
1
+
package knotserver
2
+
3
+
import (
4
+
"encoding/json"
5
+
"net/http"
6
+
)
7
+
8
+
func writeJSON(w http.ResponseWriter, data interface{}) {
9
+
w.Header().Set("Content-Type", "application/json")
10
+
w.WriteHeader(http.StatusOK)
11
+
json.NewEncoder(w).Encode(data)
12
+
}
13
+
14
+
func writeError(w http.ResponseWriter, msg string, status int) {
15
+
w.Header().Set("Content-Type", "application/json")
16
+
w.WriteHeader(status)
17
+
json.NewEncoder(w).Encode(map[string]string{"error": msg})
18
+
}
19
+
20
+
func notFound(w http.ResponseWriter) {
21
+
writeError(w, "not found", http.StatusNotFound)
22
+
}
23
+
24
+
func writeMsg(w http.ResponseWriter, msg string) {
25
+
writeJson(w, map[string]string{"msg": msg})
26
+
}
+415
knotserver/routes.go
+415
knotserver/routes.go
···
1
+
package knotserver
2
+
3
+
import (
4
+
"compress/gzip"
5
+
"errors"
6
+
"fmt"
7
+
"html/template"
8
+
"log"
9
+
"net/http"
10
+
"path/filepath"
11
+
"strconv"
12
+
"strings"
13
+
14
+
"github.com/go-chi/chi/v5"
15
+
"github.com/go-git/go-git/v5/plumbing"
16
+
"github.com/icyphox/bild/git"
17
+
"github.com/russross/blackfriday/v2"
18
+
)
19
+
20
+
func (h *Handle) Index(w http.ResponseWriter, r *http.Request) {
21
+
w.Write([]byte("This is a knot, part of the wider Tangle network: https://knots.sh"))
22
+
}
23
+
24
+
func (h *Handle) RepoIndex(w http.ResponseWriter, r *http.Request) {
25
+
path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
26
+
27
+
gr, err := git.Open(path, "")
28
+
if err != nil {
29
+
if errors.Is(err, plumbing.ErrReferenceNotFound) {
30
+
writeMsg(w, "repo empty")
31
+
return
32
+
} else {
33
+
notFound(w)
34
+
return
35
+
}
36
+
}
37
+
commits, err := gr.Commits()
38
+
if err != nil {
39
+
writeError(w, err.Error(), http.StatusInternalServerError)
40
+
log.Println(err)
41
+
return
42
+
}
43
+
44
+
var readmeContent template.HTML
45
+
for _, readme := range h.c.Repo.Readme {
46
+
ext := filepath.Ext(readme)
47
+
content, _ := gr.FileContent(readme)
48
+
if len(content) > 0 {
49
+
switch ext {
50
+
case ".md", ".mkd", ".markdown":
51
+
unsafe := blackfriday.Run(
52
+
[]byte(content),
53
+
blackfriday.WithExtensions(blackfriday.CommonExtensions),
54
+
)
55
+
html := sanitize(unsafe)
56
+
readmeContent = template.HTML(html)
57
+
default:
58
+
safe := sanitize([]byte(content))
59
+
readmeContent = template.HTML(
60
+
fmt.Sprintf(`<pre>%s</pre>`, safe),
61
+
)
62
+
}
63
+
break
64
+
}
65
+
}
66
+
67
+
if readmeContent == "" {
68
+
log.Printf("no readme found for %s", path)
69
+
}
70
+
71
+
mainBranch, err := gr.FindMainBranch(h.c.Repo.MainBranch)
72
+
if err != nil {
73
+
writeError(w, err.Error(), http.StatusInternalServerError)
74
+
log.Println(err)
75
+
return
76
+
}
77
+
78
+
if len(commits) >= 3 {
79
+
commits = commits[:3]
80
+
}
81
+
data := make(map[string]any)
82
+
data["ref"] = mainBranch
83
+
data["readme"] = readmeContent
84
+
data["commits"] = commits
85
+
data["desc"] = getDescription(path)
86
+
data["servername"] = h.c.Server.Name
87
+
data["meta"] = h.c.Meta
88
+
89
+
writeJSON(w, data)
90
+
return
91
+
}
92
+
93
+
func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) {
94
+
treePath := chi.URLParam(r, "*")
95
+
ref := chi.URLParam(r, "ref")
96
+
97
+
path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
98
+
gr, err := git.Open(path, ref)
99
+
if err != nil {
100
+
notFound(w)
101
+
return
102
+
}
103
+
104
+
files, err := gr.FileTree(treePath)
105
+
if err != nil {
106
+
writeError(w, err.Error(), http.StatusInternalServerError)
107
+
log.Println(err)
108
+
return
109
+
}
110
+
111
+
data := make(map[string]any)
112
+
data["ref"] = ref
113
+
data["parent"] = treePath
114
+
data["desc"] = getDescription(path)
115
+
data["dotdot"] = filepath.Dir(treePath)
116
+
117
+
h.listFiles(files, data, w)
118
+
return
119
+
}
120
+
121
+
func (h *Handle) FileContent(w http.ResponseWriter, r *http.Request) {
122
+
var raw bool
123
+
if rawParam, err := strconv.ParseBool(r.URL.Query().Get("raw")); err == nil {
124
+
raw = rawParam
125
+
}
126
+
127
+
treePath := chi.URLParam(r, "*")
128
+
ref := chi.URLParam(r, "ref")
129
+
130
+
path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
131
+
gr, err := git.Open(path, ref)
132
+
if err != nil {
133
+
notFound(w)
134
+
return
135
+
}
136
+
137
+
contents, err := gr.FileContent(treePath)
138
+
if err != nil {
139
+
writeError(w, err.Error(), http.StatusInternalServerError)
140
+
return
141
+
}
142
+
data := make(map[string]any)
143
+
data["ref"] = ref
144
+
data["desc"] = getDescription(path)
145
+
data["path"] = treePath
146
+
147
+
safe := sanitize([]byte(contents))
148
+
149
+
if raw {
150
+
h.showRaw(string(safe), w)
151
+
} else {
152
+
h.showFile(string(safe), data, w)
153
+
}
154
+
}
155
+
156
+
func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) {
157
+
name := displayRepoName(r)
158
+
159
+
file := chi.URLParam(r, "file")
160
+
161
+
// TODO: extend this to add more files compression (e.g.: xz)
162
+
if !strings.HasSuffix(file, ".tar.gz") {
163
+
notFound(w)
164
+
return
165
+
}
166
+
167
+
ref := strings.TrimSuffix(file, ".tar.gz")
168
+
169
+
// This allows the browser to use a proper name for the file when
170
+
// downloading
171
+
filename := fmt.Sprintf("%s-%s.tar.gz", name, ref)
172
+
setContentDisposition(w, filename)
173
+
setGZipMIME(w)
174
+
175
+
path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
176
+
gr, err := git.Open(path, ref)
177
+
if err != nil {
178
+
notFound(w)
179
+
return
180
+
}
181
+
182
+
gw := gzip.NewWriter(w)
183
+
defer gw.Close()
184
+
185
+
prefix := fmt.Sprintf("%s-%s", name, ref)
186
+
err = gr.WriteTar(gw, prefix)
187
+
if err != nil {
188
+
// once we start writing to the body we can't report error anymore
189
+
// so we are only left with printing the error.
190
+
log.Println(err)
191
+
return
192
+
}
193
+
194
+
err = gw.Flush()
195
+
if err != nil {
196
+
// once we start writing to the body we can't report error anymore
197
+
// so we are only left with printing the error.
198
+
log.Println(err)
199
+
return
200
+
}
201
+
}
202
+
203
+
func (h *Handle) Log(w http.ResponseWriter, r *http.Request) {
204
+
ref := chi.URLParam(r, "ref")
205
+
206
+
path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
207
+
gr, err := git.Open(path, ref)
208
+
if err != nil {
209
+
notFound(w)
210
+
return
211
+
}
212
+
213
+
commits, err := gr.Commits()
214
+
if err != nil {
215
+
writeError(w, err.Error(), http.StatusInternalServerError)
216
+
log.Println(err)
217
+
return
218
+
}
219
+
220
+
data := make(map[string]interface{})
221
+
data["commits"] = commits
222
+
data["meta"] = h.c.Meta
223
+
data["ref"] = ref
224
+
data["desc"] = getDescription(path)
225
+
data["log"] = true
226
+
227
+
writeJSON(w, data)
228
+
return
229
+
}
230
+
231
+
func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) {
232
+
name := displayRepoName(r)
233
+
if h.isIgnored(name) {
234
+
notFound(w)
235
+
return
236
+
}
237
+
ref := chi.URLParam(r, "ref")
238
+
239
+
path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
240
+
gr, err := git.Open(path, ref)
241
+
if err != nil {
242
+
notFound(w)
243
+
return
244
+
}
245
+
246
+
diff, err := gr.Diff()
247
+
if err != nil {
248
+
writeError(w, err.Error(), http.StatusInternalServerError)
249
+
log.Println(err)
250
+
return
251
+
}
252
+
253
+
data := make(map[string]interface{})
254
+
255
+
data["commit"] = diff.Commit
256
+
data["stat"] = diff.Stat
257
+
data["diff"] = diff.Diff
258
+
data["meta"] = h.c.Meta
259
+
data["ref"] = ref
260
+
data["desc"] = getDescription(path)
261
+
262
+
writeJSON(w, data)
263
+
return
264
+
}
265
+
266
+
func (h *Handle) Refs(w http.ResponseWriter, r *http.Request) {
267
+
path := filepath.Join(h.c.Repo.ScanPath, didPath(r))
268
+
gr, err := git.Open(path, "")
269
+
if err != nil {
270
+
notFound(w)
271
+
return
272
+
}
273
+
274
+
tags, err := gr.Tags()
275
+
if err != nil {
276
+
// Non-fatal, we *should* have at least one branch to show.
277
+
log.Println(err)
278
+
}
279
+
280
+
branches, err := gr.Branches()
281
+
if err != nil {
282
+
log.Println(err)
283
+
writeError(w, err.Error(), http.StatusInternalServerError)
284
+
return
285
+
}
286
+
287
+
data := make(map[string]interface{})
288
+
289
+
data["meta"] = h.c.Meta
290
+
data["branches"] = branches
291
+
data["tags"] = tags
292
+
data["desc"] = getDescription(path)
293
+
294
+
writeJSON(w, data)
295
+
return
296
+
}
297
+
298
+
func (h *Handle) ServeStatic(w http.ResponseWriter, r *http.Request) {
299
+
f := chi.URLParam(r, "file")
300
+
f = filepath.Clean(filepath.Join(h.c.Dirs.Static, f))
301
+
302
+
http.ServeFile(w, r, f)
303
+
}
304
+
305
+
// func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) {
306
+
// session, _ := h.s.Get(r, "bild-session")
307
+
// did := session.Values["did"].(string)
308
+
309
+
// switch r.Method {
310
+
// case http.MethodGet:
311
+
// keys, err := h.db.GetPublicKeys(did)
312
+
// if err != nil {
313
+
// h.WriteOOBNotice(w, "keys", "Failed to list keys. Try again later.")
314
+
// log.Println(err)
315
+
// return
316
+
// }
317
+
318
+
// data := make(map[string]interface{})
319
+
// data["keys"] = keys
320
+
// if err := h.t.ExecuteTemplate(w, "settings/keys", data); err != nil {
321
+
// log.Println(err)
322
+
// return
323
+
// }
324
+
// case http.MethodPut:
325
+
// key := r.FormValue("key")
326
+
// name := r.FormValue("name")
327
+
// client, _ := h.auth.AuthorizedClient(r)
328
+
329
+
// _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))
330
+
// if err != nil {
331
+
// h.WriteOOBNotice(w, "keys", "Invalid public key. Check your formatting and try again.")
332
+
// log.Printf("parsing public key: %s", err)
333
+
// return
334
+
// }
335
+
336
+
// if err := h.db.AddPublicKey(did, name, key); err != nil {
337
+
// h.WriteOOBNotice(w, "keys", "Failed to add key.")
338
+
// log.Printf("adding public key: %s", err)
339
+
// return
340
+
// }
341
+
342
+
// // store in pds too
343
+
// resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
344
+
// Collection: "sh.bild.publicKey",
345
+
// Repo: did,
346
+
// Rkey: uuid.New().String(),
347
+
// Record: &lexutil.LexiconTypeDecoder{Val: &shbild.PublicKey{
348
+
// Created: time.Now().String(),
349
+
// Key: key,
350
+
// Name: name,
351
+
// }},
352
+
// })
353
+
354
+
// // invalid record
355
+
// if err != nil {
356
+
// h.WriteOOBNotice(w, "keys", "Invalid inputs. Check your formatting and try again.")
357
+
// log.Printf("failed to create record: %s", err)
358
+
// return
359
+
// }
360
+
361
+
// log.Println("created atproto record: ", resp.Uri)
362
+
363
+
// h.WriteOOBNotice(w, "keys", "Key added!")
364
+
// return
365
+
// }
366
+
// }
367
+
368
+
// func (h *Handle) NewRepo(w http.ResponseWriter, r *http.Request) {
369
+
// session, _ := h.s.Get(r, "bild-session")
370
+
// did := session.Values["did"].(string)
371
+
// handle := session.Values["handle"].(string)
372
+
373
+
// switch r.Method {
374
+
// case http.MethodGet:
375
+
// if err := h.t.ExecuteTemplate(w, "repo/new", nil); err != nil {
376
+
// log.Println(err)
377
+
// return
378
+
// }
379
+
// case http.MethodPut:
380
+
// name := r.FormValue("name")
381
+
// description := r.FormValue("description")
382
+
383
+
// repoPath := filepath.Join(h.c.Repo.ScanPath, did, name)
384
+
// err := git.InitBare(repoPath)
385
+
// if err != nil {
386
+
// h.WriteOOBNotice(w, "repo", "Error creating repo. Try again later.")
387
+
// return
388
+
// }
389
+
390
+
// err = h.db.AddRepo(did, name, description)
391
+
// if err != nil {
392
+
// h.WriteOOBNotice(w, "repo", "Error creating repo. Try again later.")
393
+
// return
394
+
// }
395
+
396
+
// w.Header().Set("HX-Redirect", fmt.Sprintf("/@%s/%s", handle, name))
397
+
// w.WriteHeader(http.StatusOK)
398
+
// }
399
+
// }
400
+
401
+
// func (h *Handle) Timeline(w http.ResponseWriter, r *http.Request) {
402
+
// session, err := h.s.Get(r, "bild-session")
403
+
// user := make(map[string]string)
404
+
// if err != nil || session.IsNew {
405
+
// // user is not logged in
406
+
// } else {
407
+
// user["handle"] = session.Values["handle"].(string)
408
+
// user["did"] = session.Values["did"].(string)
409
+
// }
410
+
411
+
// if err := h.t.ExecuteTemplate(w, "timeline", user); err != nil {
412
+
// log.Println(err)
413
+
// return
414
+
// }
415
+
// }
+136
knotserver/util.go
+136
knotserver/util.go
···
1
+
package knotserver
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 displayRepoName(r *http.Request) string {
23
+
user := r.Context().Value("did").(string)
24
+
name := chi.URLParam(r, "name")
25
+
26
+
handle, err := auth.ResolveIdent(r.Context(), user)
27
+
if err != nil {
28
+
log.Printf("failed to resolve ident: %s: %s", user, err)
29
+
return fmt.Sprintf("%s/%s", user, name)
30
+
}
31
+
32
+
return fmt.Sprintf("@%s/%s", handle.Handle.String(), name)
33
+
}
34
+
35
+
func didPath(r *http.Request, did string) string {
36
+
path := filepath.Join(did, chi.URLParam(r, "name"))
37
+
filepath.Clean(path)
38
+
return path
39
+
}
40
+
41
+
func getDescription(path string) (desc string) {
42
+
db, err := os.ReadFile(filepath.Join(path, "description"))
43
+
if err == nil {
44
+
desc = string(db)
45
+
} else {
46
+
desc = ""
47
+
}
48
+
return
49
+
}
50
+
51
+
func (h *Handle) isUnlisted(name string) bool {
52
+
for _, i := range h.c.Repo.Unlisted {
53
+
if name == i {
54
+
return true
55
+
}
56
+
}
57
+
58
+
return false
59
+
}
60
+
61
+
func (h *Handle) isIgnored(name string) bool {
62
+
for _, i := range h.c.Repo.Ignore {
63
+
if name == i {
64
+
return true
65
+
}
66
+
}
67
+
68
+
return false
69
+
}
70
+
71
+
type repoInfo struct {
72
+
Git *git.GitRepo
73
+
Path string
74
+
Category string
75
+
}
76
+
77
+
func (d *Handle) getAllRepos() ([]repoInfo, error) {
78
+
repos := []repoInfo{}
79
+
max := strings.Count(d.c.Repo.ScanPath, string(os.PathSeparator)) + 2
80
+
81
+
err := filepath.WalkDir(d.c.Repo.ScanPath, func(path string, de fs.DirEntry, err error) error {
82
+
if err != nil {
83
+
return err
84
+
}
85
+
86
+
if de.IsDir() {
87
+
// Check if we've exceeded our recursion depth
88
+
if strings.Count(path, string(os.PathSeparator)) > max {
89
+
return fs.SkipDir
90
+
}
91
+
92
+
if d.isIgnored(path) {
93
+
return fs.SkipDir
94
+
}
95
+
96
+
// A bare repo should always have at least a HEAD file, if it
97
+
// doesn't we can continue recursing
98
+
if _, err := os.Lstat(filepath.Join(path, "HEAD")); err == nil {
99
+
repo, err := git.Open(path, "")
100
+
if err != nil {
101
+
log.Println(err)
102
+
} else {
103
+
relpath, _ := filepath.Rel(d.c.Repo.ScanPath, path)
104
+
repos = append(repos, repoInfo{
105
+
Git: repo,
106
+
Path: relpath,
107
+
Category: d.category(path),
108
+
})
109
+
// Since we found a Git repo, we don't want to recurse
110
+
// further
111
+
return fs.SkipDir
112
+
}
113
+
}
114
+
}
115
+
return nil
116
+
})
117
+
118
+
return repos, err
119
+
}
120
+
121
+
func (d *Handle) category(path string) string {
122
+
return strings.TrimPrefix(filepath.Dir(strings.TrimPrefix(path, d.c.Repo.ScanPath)), string(os.PathSeparator))
123
+
}
124
+
125
+
func setContentDisposition(w http.ResponseWriter, name string) {
126
+
h := "inline; filename=\"" + name + "\""
127
+
w.Header().Add("Content-Disposition", h)
128
+
}
129
+
130
+
func setGZipMIME(w http.ResponseWriter) {
131
+
setMIME(w, "application/gzip")
132
+
}
133
+
134
+
func setMIME(w http.ResponseWriter, mime string) {
135
+
w.Header().Add("Content-Type", mime)
136
+
}